Adding additional claims to access_token (asp.net core 3.1 mvc)

Hello, I’m quite new to OIDC in general, so apologies in advance if this is a stupid question.

I’m following the quickstart guides:

Everything is working as expected:

  1. When I browse to an [Authorize] route in MVC, I get redirected to Auth0 login. I get back an id_token and access_token after logging in.
  2. I can use the access_token to access [Authorize] endpoints in the API.

Question: Is it possible to force additional claims into the access_token? If yes, how do I do this?

For context, this is an example of the decoded access_token:
{
“alg”: “RS256”,
“typ”: “JWT”,
“kid”: “Oh3auIcHF7FKoi_maBqKe”
}.{
“iss”: “https://blah.au.auth0.com/”,
“sub”: “auth0|5ebb0ff83873a20be682a54b”,
“aud”: [
https://api.blah.net”,
https://blah.au.auth0.com/userinfo
],
“iat”: 1589924568,
“exp”: 1590010968,
“azp”: “2wDvZX5y1vXRj70U37hIzezV2IPDqgbt”,
“scope”: “openid profile email read:messages”
}.[Signature]

The reason for this is that in my current environment, existing user data is typically stored with email address as the primary key. When the API endpoints are called, it needs to access the user’s data using their email address.

From all the reading I’ve done so far, the typical answer is to call the /userinfo endpoint to obtain additional information about a user.

So I could get that information by doing a POST to https://blah.au.auth0.com/userinfo using the access token.

Something like the code below:

    Microsoft.Extensions.Primitives.StringValues authTokens;
    HttpContext.Request.Headers.TryGetValue("Authorization", out authTokens);

    var accessToken = authTokens.FirstOrDefault();

    var request = new HttpRequestMessage(HttpMethod.Get,
    "https://blah.au.auth0.com/userinfo");
    request.Headers.Add("Authorization", accessToken);

    var client = _clientFactory.CreateClient();
    var response = await client.SendAsync(request);

However, this doesn’t seem very scalable. If I have 5 endpoints on that API, all of which require the user’s email address, that’s already at least 5 calls to /userinfo (just for a single user).

It would be much easier if the required information was included in the access_token.

This is the MVC services code:

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<CookiePolicyOptions>(options =>
    {
        // This lambda determines whether user consent for non-essential cookies is needed for a given request.
        options.CheckConsentNeeded = context => HostingEnvironment.IsProduction();
        options.MinimumSameSitePolicy = SameSiteMode.None;
    });

    // Add authentication services
    services.AddAuthentication(options => {
        options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    })
    .AddCookie()
    .AddOpenIdConnect("Auth0", options => {
        // Set the authority to your Auth0 domain
        options.Authority = $"https://{Configuration["Auth0:Domain"]}";

        // Configure the Auth0 Client ID and Client Secret
        options.ClientId = Configuration["Auth0:ClientId"];
        options.ClientSecret = Configuration["Auth0:ClientSecret"];

        // Set response type to code
        options.ResponseType = "code";

        // Configure the scope
        options.Scope.Clear();
        options.Scope.Add("openid");
        options.Scope.Add("profile");
        options.Scope.Add("email");
        options.Scope.Add("read:messages");

        // Set the callback path, so Auth0 will call back to http://localhost:3000/callback
        // Also ensure that you have added the URL as an Allowed Callback URL in your Auth0 dashboard
        options.CallbackPath = new PathString("/callback");

        // Configure the Claims Issuer to be Auth0
        options.ClaimsIssuer = "Auth0";


        // Saves tokens to the AuthenticationProperties
        options.SaveTokens = true;

        options.Events = new OpenIdConnectEvents
        {
            // If you want to call an API from your MVC application, you need to obtain an Access Token issued for the API you want to call.
            // To obtain the token, pass an additional audience parameter containing the API identifier to the Auth0 authorization endpoint.
            OnRedirectToIdentityProvider = context =>
            {
                context.ProtocolMessage.SetParameter("audience", "https://api.blah.net");

                return Task.FromResult(0);
            },

            // handle the logout redirection 
            OnRedirectToIdentityProviderForSignOut = (context) =>
            {
                var logoutUri = $"https://{Configuration["Auth0:Domain"]}/v2/logout?client_id={Configuration["Auth0:ClientId"]}";

                var postLogoutUri = context.Properties.RedirectUri;
                if (!string.IsNullOrEmpty(postLogoutUri))
                {
                    if (postLogoutUri.StartsWith("/"))
                    {
                        // transform to absolute
                        var request = context.Request;
                        postLogoutUri = request.Scheme + "://" + request.Host + request.PathBase + postLogoutUri;
                    }
                    logoutUri += $"&returnTo={ Uri.EscapeDataString(postLogoutUri)}";
                }

                context.Response.Redirect(logoutUri);
                context.HandleResponse();

                return Task.CompletedTask;
            }
        };
    });

    services.AddControllersWithViews();
}

Hi @jorlee

You are saving the tokens on authentication by use of the SaveTokens flag in your OpenIDConnect options:

options.SaveTokens = true;

With this flag set to true, you can retrieve the ID token to get the user’s email address without calling the /UserInfo endpoint each time:

// Inside one of your controller actions

if (User.Identity.IsAuthenticated)
{
    string accessToken = await HttpContext.GetTokenAsync("access_token");
    
    // if you need to check the Access Token expiration time, use this value
    // provided on the authorization response and stored.
    // do not attempt to inspect/decode the access token
    DateTime accessTokenExpiresAt = DateTime.Parse(
        await HttpContext.GetTokenAsync("expires_at"), 
        CultureInfo.InvariantCulture,
        DateTimeStyles.RoundtripKind);
        
    string idToken = await HttpContext.GetTokenAsync("id_token");

    // Now you can use them. For more info on when and how to use the
    // Access Token and ID Token, see https://auth0.com/docs/tokens
}

Hi @andy.carter thanks for replying!
The options.SaveTokens = true; line is in the MVC application though.

What I was asking about is on the API side.
So if the MVC application is calling an API, it sends the access_token to the API.
However on the API side, it won’t have any of the information contained in the id_token (as it only has the access_token).

Hi @jorlee,

Thanks for the update!

In this scenario, normally the sub claim would be used to contextually identify the user, but obviously that won’t work for your use case with data stored against email address.

You can add the email address for the user to the access token using a rule:

It’s important to note that by default, Auth0 always enforces namespacing; any custom claims with non-namespaced identifiers will be silently excluded from tokens.

function(user, context, callback) {
  const namespace = 'https://your.namespace.com/';
  context.accessToken[namespace + 'email'] = user.email;

  callback(null, user, context);
}

Your API can then use this claim as part of its operations without needing to make an extra API call to get the user’s email address.

1 Like

Thanks @andy.carter! That’s perfect!

For anybody else interested, if you apply the rule in Andy’s reply, this is what the access token then looks like:

{
  "alg": "RS256",
  "typ": "JWT",
  "kid": "Oh3auIcHF7FKoi_maBqKe"
}.{
  "https://blah.namespace.com/email": "test@contoso.com",
  "iss": "https://blah.au.auth0.com/",
  "sub": "auth0|5ebb0ff83873a20be682a54b",
  "aud": [
    "https://api.blah.net",
    "https://blah.au.auth0.com/userinfo"
  ],
  "iat": 1590089834,
  "exp": 1590176234,
  "azp": "2wDvZX5y1vXRj70U37hIzezV2IPDqgbt",
  "scope": "openid profile"
}.[Signature]

In the API, you can then extract the email with the following code:

var email = User.Claims.FirstOrDefault(c => c.Type == "https://blah.namespace.com/email").Value;

Cheers!

1 Like

Thanks for sharing that @jorlee with the rest of community!