Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

OAuth with custom JWT authentication

I'm struggling to implement a custom auth flow with OAuth and JWT. Basically it should go as follows:

  • User clicks in login
  • User is redirect to 3rd party OAuth login page
  • User logins into the page
  • I get the access_token and request for User Info
  • I get the user info and create my own JWT Token to be sent back and forth

I have been following this great tutorial on how to build an OAuth authentication, the only part that differs is that Jerrie is using Cookies.

What I Have done so far:

Configured the AuthenticationService

services.AddAuthentication(options => 
{
    options.DefaultChallengeScheme = "3rdPartyOAuth";
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie() // Added only because of the DefaultSignInScheme
.AddJwtBearer(options =>
{
    options.TokenValidationParameters = // Ommited for brevity
})
.AddOAuth("3rdPartyOAuth", options =>
{
    options.ClientId = securityConfig.ClientId;
    options.ClientSecret = securityConfig.ClientSecret;
    options.CallbackPath = new PathString("/auth/oauthCallback");

    options.AuthorizationEndpoint = securityConfig.AuthorizationEndpoint;
    options.TokenEndpoint = securityConfig.TokenEndpoint;
    options.UserInformationEndpoint = securityConfig.UserInfoEndpoint;

    // Only this for testing for now
    options.ClaimActions.MapJsonKey("sub", "sub");

    options.Events = new OAuthEvents
    {
        OnCreatingTicket = async context =>
        {
            // Request for user information
            var request = new HttpRequestMessage(HttpMethod.Get, context.Options.UserInformationEndpoint);
            request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
            request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", context.AccessToken);

            var response = await context.Backchannel.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, context.HttpContext.RequestAborted);
            response.EnsureSuccessStatusCode();

            var user = JObject.Parse(await response.Content.ReadAsStringAsync());

            context.RunClaimActions(user);
        }
    };
});

Auth Controller

    [AllowAnonymous]
    [HttpGet("login")]
    public IActionResult LoginIam(string returnUrl = "/auth/loginCallback")
    {
        return Challenge(new AuthenticationProperties() {RedirectUri = returnUrl});
    }

    [AllowAnonymous]
    [DisableRequestSizeLimit]
    [HttpGet("loginCallback")]
    public IActionResult IamCallback()
    {
        // Here is where I expect to get the user info, create my JWT and send it back to the client
        return Ok();
    }

Disclaimer: This OAuth flow is being incorporated now. I have a flow for creating and using my own JWT working and everything. I will not post here because my problem is before that.

What I want

In Jerrie's post you can see that he sets DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;. With that, when the /auth/loginCallback is reached I have the user claims in the HttpContext. The problem is my DefaultAuthenticateScheme is set to JwtBearersDefault and when the loginCallback is called I can't see the user claims nowhere in the Request. How can I have access to the information gained on the OnCreatingTicketEvent in my callback in this scenario?

Bonus question: I don't know much about OAuth (sure that is clear now). You may have noted that my options.CallbackPath differs from the RedirectUri passed in the Challenge at the login endpoint. I expected the option.CallbackPath to be called by the 3rd Part OAuth provider but this is not what happens (apparently). I did have to set the CallbackPath to the same value I have set in the OAuth provider configuration (like Jerries tutorial with GitHub) for it to work. Is that right? The Callback is used for nothing but a match configuration? I can even comment the endpoint CallbackPath points to and it keep working the same way...

Thanks!

like image 856
João Menighin Avatar asked Mar 28 '19 13:03

João Menighin


1 Answers

Auth

As Jerrie linked in his post, there is a great explanation about auth middlewares: https://digitalmccullough.com/posts/aspnetcore-auth-system-demystified.html

You can see a flowchart in the section Authentication and Authorization Flow

The second step is Authentication middleware calls Default Handler's Authenticate.

As your default auth handler is Jwt, the context is not pupulated with the user data after the oauth flow, since it uses the CookieAuthenticationDefaults.AuthenticationScheme

Try:

[AllowAnonymous]
[DisableRequestSizeLimit]
[HttpGet("loginCallback")]
public IActionResult IamCallback()
{
    //
    // Read external identity from the temporary cookie
    //
    var result = await HttpContext.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme);
    
    if (result?.Succeeded != true)
    {
        throw new Exception("Nein");
    }

    var oauthUser = result.Principal;

    ...

    return Ok();
}

Great schemes summary: ASP.NET Core 2 AuthenticationSchemes

You can persist your user with https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationhttpcontextextensions.signinasync?view=aspnetcore-2.2

Bonus

I did have to set the CallbackPath to the same value I have set in the OAuth provider configuration (like Jerries tutorial with GitHub) for it to work. Is that right?"

Yes. For security reasons, the registered callback uri (on authorization server) and the provided callback uri (sent by the client) MUST match. So you cannot change it randomly, or if you change it, you have to change it on the auth server too.

If this restriction was not present, f.e. an email with a mailformed link (with modified callback url) could obtain grant. This is called Open Redirect, the rfc refers to it too: https://www.rfc-editor.org/rfc/rfc6749#section-10.15

OWASP has a great description: https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md

I can even comment the endpoint CallbackPath points to and it keep working the same way..."

That is because your client is trusted (you provide your secret, and you are not a fully-frontend Single Page App). So it is optional for you to send the callback uri. But IF you send it, it MUST match with the one registered on the server. If you don't send it, the auth server will redirect to the url, that is registered on its side.

https://www.rfc-editor.org/rfc/rfc6749#section-4.1.1

redirect_uri OPTIONAL. As described in Section 3.1.2.

https://www.rfc-editor.org/rfc/rfc6749#section-3.1.2

The authorization server redirects the user-agent to the client's redirection endpoint previously established with the authorization server during the client registration process or when making the authorization request.

https://www.rfc-editor.org/rfc/rfc6749#section-3.1.2.2

The authorization server MUST require the following clients to register their redirection endpoint:

  • Public clients.
  • Confidential clients utilizing the implicit grant type.

Your client is confidential and uses authorization code grant type (https://www.rfc-editor.org/rfc/rfc6749#section-1.3.1)

https://www.rfc-editor.org/rfc/rfc6749#section-3.1.2.3

If multiple redirection URIs have been registered, if only part of the redirection URI has been registered, or if no redirection URI has been registered, the client MUST include a redirection URI with the authorization request using the "redirect_uri" request parameter.

You have registered your redirect uri, that's why the auth server does not raise an error.

like image 85
LeBoucher Avatar answered Oct 13 '22 15:10

LeBoucher