skip to Main Content

This topic is one that feels like it should be documented better – or perhaps I am using the wrong terms when searching.

  • I have several SPA apps that use various Oauth2 logins
    (ie. Okta, Facebook, Google) to authenticate and generate access
    tokens.

  • These apps all access a common API backend (asp.net core). All
    requests to the API have the Oauth2 access token attached as an Authorization header.

How do I configure this single backend API to validate these access tokens from one of a variety of providers, without knowing in advance which access token is attached, and decode a user email address that I can use for further authorization purposes?

I have found much documentation on validating tokens from a descrete, known authorization provider, but very little on using multiple providers. With all the apps out there that give you a choice of Oauth2 logons to choose from (StackOverflow among them), I thought this would be a more common problem.

What am I missing!?

3

Answers


  1. Chosen as BEST ANSWER

    It seems like the correct way to address this situation is to build a Custom Authentication Handler as documented here: https://referbruv.com/blog/posts/implementing-custom-authentication-scheme-and-handler-in-aspnet-core-3x

    In this Authentication Handler I can decode the token, assert that the issuer is a member of a whitelist, validate the access token using the issuer's public key, and use the rest of the token to build the Identity I need for further authorization.

    At least now I have a better idea what to search for, and I'm not completely re-inventing the authentication mechanism!


  2. You will want to identify the user in a consistent way in your APIs, then authorize requests based on the identity + scopes.

    This will be very difficult when using many different token providers, as you are finding. Their access tokens are not designed for you to use in your own APIs.

    A better mechanism is to use tokens only from your own Authorization Server, to support different login methods but also put your code in control. My Federated Logins blog post has further info.

    Login or Signup to reply.
  3. It turns out I was overthinking this after all.

    Since I am dealing with an API backend, all I needed to do was to validate IDP Bearer tokens, not to create them. In the end, I was able to validate 3 ID providers using the folowing simple code:

    services.AddAuthentication(OKTA_SCHEME)
                    .AddJwtBearer(ADFS_SCHEME, options =>
                    {
                        options.Authority = adfsConfig.authority;
                        options.Authority = adfsConfig.authority;
                    })
                    .AddJwtBearer(GOOGLE_SCHEME, jwt => jwt.UseGoogle(
                        clientId: googleConfig.clientId
                    ))
                    .AddJwtBearer(OKTA_SCHEME, options =>
                    {
                        options.Authority = oktaConfig.authority;
                        options.Audience = oktaConfig.audience;
                    });
    

    Note that this required the installation of one additional nuget package to simplify the validation of the Google tokens, which don’t appear to follow the standard: Hellang.Authentication.JwtBearer.Google.

    At this point I can authorize using attributes like:

    [Authorize(AuthorizationSchemes = OKTA_SCHEME)]
    

    …or set up policies based on the schemes.

    The second part problem was to link my various logons to users in a local database, which I ended up doing using a custom IClaimsTransformation that uses the information populated to ClaimsPrincipal to lookup a the user in my database, and add an "Employee" role claim, if they are found.

    public class EmployeeClaims : IClaimsTransformation
    {
        public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
        {
            if (!principal.HasClaim(a => a.Type == "EmployeeNumber"))
            {
                Employee employee = lookupEmployee(principal);
                if (employee != null)
                {
                    ClaimsIdentity id = new ClaimsIdentity();
                    id.AddClaim(new Claim(ClaimTypes.Role, "Employee"));
                    id.AddClaim(new Claim("EmployeeNumber", employee.EmployeeNumber.ToString()));
                    principal.AddIdentity(id);
                }
            }
            return Task.FromResult(principal);
        }
        private Employee lookupEmployee(ClaimsPrincipal principal) {
            string issuer = principal.Claims.Single(a => a.Type == "iss").Value;
            if (issuer.Contains("google.com"))
            ...
        }
    }
    

    This IClaimsTransformation is then registered by:

    services.AddScoped<IClaimsTransformation, EmployeeClaims>();
    

    Now I can additionally authorize employees with:

    [Authorize(Roles = "Employee")]
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search