1

My team and I created a custom OAuth to be used for external SSO. It works on localhost but as soon as we take it up to our staging environment we get an "The oauth state was missing or invalid." error.

We used "https://auth0.com/" for testing.

To try and troubleshoot this we overrode the following built in methods and via breakpoints can see that Query state comes back null.

protected override string BuildChallengeUrl(AuthenticationProperties properties, string redirectUri);
protected override async Task<HandleRequestResult> HandleRemoteAuthenticateAsync();

I need some help figuring out why this is a problem on staging and not on local as we are a bit stumped. A theory we have is that the decoder used inside these methods change on var properties = Options.StateDataFormat.Unprotect(state); and thus because they aren't the same they can't decode each others states. I will put our implementation below, if its required I can paste the built in methods as well but I can't fathom the problem lying with the built in functions.

Startup:

foreach (var customAuthItem in customAuthList)
                {
                    services.AddAuthentication().AddCustom(customAuthItem.CampaignId, options =>
                    {
                        options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
                        options.AuthorizationEndpoint = customAuthItem.AuthEndpoint;
                        options.TokenEndpoint = customAuthItem.TokenEndpoint;
                        options.UserInformationEndpoint = customAuthItem.UserInfoEndpoint;
                        options.ClientId = customAuthItem.ClientId;
                        options.ClientSecret = customAuthItem.ClientSecret;
                    });
                }

Options:

public class CustomAuthenticationOptions : OAuthOptions
{
    /// <summary>
    /// Initializes a new instance of the <see cref="CustomAuthenticationOptions"/> class.
    /// </summary>
    public CustomAuthenticationOptions()
    {
        ClaimsIssuer = CustomAuthenticationDefaults.Issuer;
        CallbackPath = CustomAuthenticationDefaults.CallbackPath;

        AuthorizationEndpoint = CustomAuthenticationDefaults.AuthorizationEndpoint;
        TokenEndpoint = CustomAuthenticationDefaults.TokenEndpoint;
        UserInformationEndpoint = CustomAuthenticationDefaults.UserInformationEndpoint;

        Scope.Add("openid");
        Scope.Add("profile");
        Scope.Add("email");

        ClaimActions.MapJsonKey(ClaimTypes.Email, "email");
        ClaimActions.MapJsonKey(ClaimTypes.Name, "name");
        ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "sub");
    }

    /// Gets the list of fields to retrieve from the user information endpoint.
    /// </summary>
    public ISet<string> Fields { get; } = new HashSet<string>
    {
        "email",
        "name",
        "sub"
    };

Defaults:

public static class CustomAuthenticationDefaults
{
    /// <summary>
    /// Default value for <see cref="AuthenticationScheme.Name"/>.
    /// </summary>
    public const string AuthenticationScheme = "Custom";

    /// <summary>
    /// Default value for <see cref="AuthenticationScheme.DisplayName"/>.
    /// </summary>
    public static readonly string DisplayName = "Custom";

    /// <summary>
    /// Default value for <see cref="AuthenticationSchemeOptions.ClaimsIssuer"/>.
    /// </summary>
    public static readonly string Issuer = "Custom";

    /// <summary>
    /// Default value for <see cref="RemoteAuthenticationOptions.CallbackPath"/>.
    /// </summary>
    public static readonly string CallbackPath = "/signin-custom";

    /// <summary>
    /// Default value for <see cref="OAuthOptions.AuthorizationEndpoint"/>.
    /// </summary>
    public static readonly string AuthorizationEndpoint = "https://dev-egd511ku.us.auth0.com/authorize";

    /// <summary>
    /// Default value for <see cref="OAuthOptions.TokenEndpoint"/>.
    /// </summary>
    public static readonly string TokenEndpoint = "https://dev-egd511ku.us.auth0.com/oauth/token";

    /// <summary>
    /// Default value for <see cref="OAuthOptions.UserInformationEndpoint"/>.
    /// </summary>
    public static readonly string UserInformationEndpoint = "https://dev-egd511ku.us.auth0.com/userinfo";
}

Handler:

protected override async Task<AuthenticationTicket> CreateTicketAsync(
        [NotNull] ClaimsIdentity identity,
        [NotNull] AuthenticationProperties properties,
        [NotNull] OAuthTokenResponse tokens)
    {
        Serilog.Log.Debug("CustomAuthenticationHandler.CreateTicketAsync: STARTED!");

        string endpoint = Options.UserInformationEndpoint;

        if (Options.Fields.Count > 0)
        {
            endpoint = QueryHelpers.AddQueryString(endpoint, "fields", string.Join(',', Options.Fields));
        }

        using var request = new HttpRequestMessage(HttpMethod.Get, endpoint);
        request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
        request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokens.AccessToken);

        Serilog.Log.Debug("CustomAuthenticationHandler.CreateTicketAsync: ABOUT TO SEND REQUEST!");

        using var response = await Backchannel.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, Context.RequestAborted);
        if (!response.IsSuccessStatusCode)
        {
            Serilog.Log.Debug($"CustomAuthenticationHandler.CreateTicketAsync: FAILED REQUEST: {response.ReasonPhrase}");

            await Log.UserProfileErrorAsync(Logger, response, Context.RequestAborted);
            throw new HttpRequestException("An error occurred while retrieving the user profile from Custom.");
        }

        var payloadString = await response.Content.ReadAsStringAsync();

        Serilog.Log.Debug($"CustomAuthenticationHandler.CreateTicketAsync: PAYLOAD: {payloadString}");

        using var payload = JsonDocument.Parse(payloadString);// Context.RequestAborted));

        var principal = new ClaimsPrincipal(identity);
        var context = new OAuthCreatingTicketContext(principal, properties, Context, Scheme, Options, Backchannel, tokens, payload.RootElement);
        context.RunClaimActions();

        await Events.CreatingTicket(context);
        return new AuthenticationTicket(context.Principal!, context.Properties, Scheme.Name);
    }

EDIT: The error is received after successful login and after being redirected back to our site. I can see through sentry breadcrumbs that the states are correct so it seems to be a decryption issue.

5
  • Wild guess its something in your CallbackPath that might give you something to look into until someone else pops in with the anwser. Commented May 10, 2022 at 8:59
  • I'll look around with the CallbackPath though I am uncertain if its that because the error is received post login on being redirected back to our site. So the site is hit correctly but this state parameter is can't be decrypted. Commented May 10, 2022 at 9:20
  • I URL can have parameters after the question mark, oauth uses the parameters. It looks like you may be missing these parameters. See following : iana.org/assignments/oauth-parameters/oauth-parameters.xhtml Commented May 10, 2022 at 10:27
  • I do have the url parameters. If you could perhaps be a bit more specific what you think is missing because as I've mentioned the sso does work on local and it does go through all the way to the redirect back to our site (post login) on staging en then breaks at the very last step. Commented May 11, 2022 at 7:34
  • are you able to fix this issue? I have fix it by adding unique CallbackPath value for each custom option Commented May 15, 2023 at 16:17

1 Answer 1

0

It turns out the problem is because I AddAuthentication() twice, it ignores the follow-up registration of the auth methods, resulting in only one OAuth working. This is a bit of a problem because we want to support multiple SSO options for our clients, but might need to figure out a different approach. I am just glad I finally know where the problem is.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.