r3plica
r3plica

Reputation: 13387

Web API 2 /Token return more information

I am using OAuth to generate tokens for my application, specifically JWT. I have this code in my startup class:

private void ConfigureOAuthTokenGeneration(IAppBuilder app)
{

    // Configure the db context and user manager to use a single instance per request
    app.CreatePerOwinContext(DatabaseContext.Create);
    app.CreatePerOwinContext<UserService>(UserService.Create);
    app.CreatePerOwinContext<RoleService>(RoleService.Create);

    // Plugin the OAuth bearer JSON Web Token tokens generation and Consumption will be here
    var OAuthServerOptions = new OAuthAuthorizationServerOptions()
    {
        //For Dev enviroment only (on production should be AllowInsecureHttp = false)
        AllowInsecureHttp = true,
        TokenEndpointPath = new PathString("/oauth/token"),
        AccessTokenExpireTimeSpan = TimeSpan.FromDays(1),
        Provider = new OAuthProvider(),
        AccessTokenFormat = new CustomJwtFormat("http://localhost:58127")
    };

    // OAuth 2.0 Bearer Access Token Generation
    app.UseOAuthAuthorizationServer(OAuthServerOptions);
}

As you can see I have set a custom OAuthProvider and for the AccessTokenFormat I am using a CustomJwtFormat. The OAuthProvider looks like this:

public class OAuthProvider : OAuthAuthorizationServerProvider
{

    /// <summary>
    /// Validate client authentication
    /// </summary>
    /// <param name="context">The current context</param>
    /// <returns></returns>
    public override Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
    {

        // Validate all requests (because our front end is trusted)
        context.Validated();

        // Return nothing
        return Task.FromResult<object>(null);
    }

    /// <summary>
    /// Validate user credentials
    /// </summary>
    /// <param name="context">The current context</param>
    /// <returns></returns>
    public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
    {

        // Allow any origin
        var allowedOrigin = "*";

        // Add the access control allow all to our headers
        context.OwinContext.Response.Headers.Add("Access-Control-Allow-Origin", new[] { allowedOrigin });

        // Get our user service
        var service = context.OwinContext.GetUserManager<UserService>();

        // Find out user
        var user = await service.FindAsync(context.UserName, context.Password);

        // If the user is not found
        if (user == null)
        {

            // Set an error
            context.SetError("invalid_grant", "The user name or password is incorrect.");

            // Return from the function
            return;
        }

        // If the user has not confirmed their account
        if (!user.EmailConfirmed)
        {

            // Set an error
            context.SetError("invalid_grant", "User did not confirm email.");

            // Return from the function
            return;
        }

        // Generate the identity for the user
        var oAuthIdentity = await user.GenerateUserIdentityAsync(service, "JWT");

        // Create a new ticket
        var ticket = new AuthenticationTicket(oAuthIdentity, null);

        // Add the ticked to the validated context
        context.Validated(ticket);
    }
}

which is pretty straight forward. Also, the CustomJwtFormat class looks like this:

/// <summary>
/// JWT Format
/// </summary>
public class CustomJwtFormat : ISecureDataFormat<AuthenticationTicket>
{

    // Create our private property
    private readonly string issuer;

    /// <summary>
    /// Default constructor
    /// </summary>
    /// <param name="issuer">The issuer</param>
    public CustomJwtFormat(string issuer)
    {
        this.issuer = issuer;
    }

    /// <summary>
    /// Method to create our JWT token
    /// </summary>
    /// <param name="data">The Authentication ticket</param>
    /// <returns></returns>
    public string Protect(AuthenticationTicket data)
    {

        // If no data is supplied, throw an exception
        if (data == null)
            throw new ArgumentNullException("data");

        // Get our values from our appSettings
        string audienceId = ConfigurationManager.AppSettings["as:AudienceId"];
        string symmetricKeyAsBase64 = ConfigurationManager.AppSettings["as:AudienceSecret"];

        // Decode our secret and encrypt the bytes
        var keyByteArray = TextEncodings.Base64Url.Decode(symmetricKeyAsBase64);
        var signingKey = new HmacSigningCredentials(keyByteArray);

        // Get our issue and expire dates in UNIX timestamps
        var issued = data.Properties.IssuedUtc;
        var expires = data.Properties.ExpiresUtc;

        // Create our new token
        var token = new JwtSecurityToken(this.issuer, audienceId, data.Identity.Claims, issued.Value.UtcDateTime, expires.Value.UtcDateTime, signingKey);

        // Create a handler
        var handler = new JwtSecurityTokenHandler();

        // Write our token string
        var jwt = handler.WriteToken(token);

        // Return our token string
        return jwt;
    }

    /// <summary>
    /// 
    /// </summary>
    /// <param name="protectedText"></param>
    /// <returns></returns>
    public AuthenticationTicket Unprotect(string protectedText)
    {
        throw new NotImplementedException();
    }
}

This is the crucial bit. It uses Thinktecture to generate the token. The line that looks like this:

// Create our new token
var token = new JwtSecurityToken(this.issuer, audienceId, data.Identity.Claims, issued.Value.UtcDateTime, expires.Value.UtcDateTime, signingKey);

// Create a handler
var handler = new JwtSecurityTokenHandler();

// Write our token string
var jwt = handler.WriteToken(token);

This returns what you would expect from the token (access_token, expires_in and token_type) but I would like to return some user information too. Things like username, roles, etc.

Anyone know how I can do this?

Upvotes: 2

Views: 1198

Answers (1)

Spencer
Spencer

Reputation: 271

The username and roles are present in the authenticated identities claims and therefore persisted in the JWT that is sent back in the access token.

So the line: var token = new JwtSecurityToken(_issuer, audienceId, data.Identity.Claims,issued,expires,signingKey);

Inserts the claims from the auth'd identity:

i.e if I did this in the OAuth provider:

```

IList<Claim> claims = new List<Claim>();

if (context.UserName.Equals("spencer") && context.UserName.Equals(context.Password))
        {                
            claims.Add(new Claim(ClaimTypes.Name, user.DisplayName));
            claims.Add(new Claim(ClaimTypes.Role, "User"));
        }
));

var claimIdentity = new ClaimsIdentity(claims);
var ticket = new AuthenticationTicket(claimIdentity, null);

//Now authed and claims are in my identity context
context.Validated(ticket);

```

So now when the JWT is generated those claims are in the token.

You can then decorate your Api Controllers with explicit roles which would then query the "Roles" type in the claimset. If the user doesn't have the role in the role claimset than a 401 is issued:

```

[Route]
[Authorize(Roles ="User,Admin")]
public IHttpActionResult Get()
{
    return Ok<IEnumerable<Product>>(_products);
}

[Route]
[Authorize(Roles = "Admin")]
public IHttpActionResult Post(Product product)
{
    _products.Add(product);
    return Created(string.Empty, product);
}

```

So in the above example, if I generate a JWT as me "Spencer" i'm in the users role and the GET would be OK (200) whilst the POST would be Unauthorized (401).

Make sense?

Upvotes: 0

Related Questions