With a new year comes another exciting release of the Stormpath .NET SDK. In this release, we’ve knocked down another big milestone: full support for token-based authentication. But that’s not all! This release also includes support for JSON Web Tokens (JWTs), and contains plenty of improvements along the way.

Why OAuth and Token-Based Authentication Is Awesome For .NET Apps

OAuth and token-based authentication are a common point of confusion and frustration for web developers. OAuth is not Single Sign-On, but it’s often confused with SSO because many login providers (such as Google and Facebook) use a flavor of OAuth to handle external login. At the most basic level, OAuth means generating access and refresh tokens when users log into your application. And tokens are awesome!

Let me explain why.

Think of an access token as representing the identity of a user who is logged into your application. The access token is sent to the server with every request. On the server, the token is examined and verified to ensure the request is valid and authorized. After a period of time, the token expires and is no longer valid.

When an access token expires, the refresh token is used to generate a fresh access token. Typically, access tokens are short-lived and refresh tokens are long-lived. If the access token is compromised, it can be revoked, which forces the generation of a new access token via the user’s refresh token. Likewise, if a refresh token is compromised, it can be revoked so it cannot be used to generate new access tokens.

Why are tokens awesome? In short: scalability and security.

HTTP is stateless, so there’s no built-in way to store session or user information between requests. In the past, .NET developers had to rely on cookies and session variables to maintain state on the server, but this can be insecure. And if you’re planning on hosting your application on multiple servers behind a load balancer, making sure sessions are correctly shared between your machines can be a huge pain.

Token-based authentication is stateless, just like HTTP. Access and refresh tokens (in the form of JSON Web Tokens) are stored client-side and represent the user’s identity and authorization claims. Nothing is stored on the server to represent the session, so it’s easier to scale horizontally on multiple machines.

A cryptographic signature is used to ensure that the token isn’t compromised. An ID claim can be included in each token to prevent replay attacks. Additionally, if you know a user or token has been compromised, you can revoke their tokens immediately and prevent any future requests. This improves the security of the application.

In this release of the Stormpath .NET SDK, we’ve added rich support for the token management features already available in the Stormpath REST API.

Install the .NET SDK

Installing the SDK in a new project is as simple as:

install-package Stormpath.SDK

If you’re already using the SDK, this release should be a drop-in upgrade without any breaking changes:

update-package Stormpath.SDK

For a full list of changes, see the changelog on Github.

How Stormpath Makes Token Management In .NET Easy

With the Stormpath .NET SDK, it’s easy to:

Set policies for access and refresh token expiry

Generate access tokens and refresh tokens when users log into your application

Verify that an access token hasn’t been revoked or tampered with

Use a refresh token to generate a new access token

List tokens for an account and application

Revoke access and refresh tokens

Configure Access and Refresh Token Policies

Each Application has an OAuth Policy, which can be retrieved by calling GetOauthPolicyAsync :

var policy = await myApp.GetOauthPolicyAsync(); // Default values: // policy.AccessTokenTimeToLive = TimeSpan.FromHours(1); // policy.RefreshTokenTimeToLive = TimeSpan.FromDays(60); 1 2 3 4 5 var policy = await myApp . GetOauthPolicyAsync ( ) ; // Default values: // policy.AccessTokenTimeToLive = TimeSpan.FromHours(1); // policy.RefreshTokenTimeToLive = TimeSpan.FromDays(60);

These values can be updated by the appropriate Set methods, and then calling SaveAsync :

policy.SetAccessTokenTimeToLive(TimeSpan.FromDays(1)); policy.SetRefreshTokenTimeToLive(TimeSpan.FromDays(180)); await policy.SaveAsync(); 1 2 3 4 policy . SetAccessTokenTimeToLive ( TimeSpan . FromDays ( 1 ) ) ; policy . SetRefreshTokenTimeToLive ( TimeSpan . FromDays ( 180 ) ) ; await policy . SaveAsync ( ) ;

The maximum duration for access and refresh tokens is 180 days. Refresh tokens are optional; to disable refresh token generation, set the Refresh Token TTL to TimeSpan.Zero .

Generate OAuth Tokens for a User

Token generation should happen when a user logs into your application. In OAuth parlance, your application will issue a Password Grant request to Stormpath containing the user’s credentials. If the credentials are valid, Stormpath will generate access and refresh tokens for the user.

Given a set of user credentials, you can generate tokens with a few lines of code:

// Build the Password Grant request var passwordGrantRequest = OauthRequests.NewPasswordGrantRequest() .SetLogin("lskywalker@tattooine.rim") .SetPassword("whataPieceofjunk$1138") .Build(); // Send the request to Stormpath var grantResult = await myApp.NewPasswordGrantAuthenticator() .AuthenticateAsync(passwordGrantRequest); // grantResult.AccessTokenHref is the location of the created Access Token resource in Stormpath // grantResult.ExpiresIn is the number of seconds the access token is valid // grantResult.AccessTokenString is the access token JWT // grantResult.RefreshTokenString is the refresh token JWT // grantResult.GetAccessTokenAsync() will retrieve the resource as an IAccessToken 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 // Build the Password Grant request var passwordGrantRequest = OauthRequests . NewPasswordGrantRequest ( ) . SetLogin ( [email protected]" ) . SetPassword ( "whataPieceofjunk$1138" ) . Build ( ) ; // Send the request to Stormpath var grantResult = await myApp . NewPasswordGrantAuthenticator ( ) . AuthenticateAsync ( passwordGrantRequest ) ; // grantResult.AccessTokenHref is the location of the created Access Token resource in Stormpath // grantResult.ExpiresIn is the number of seconds the access token is valid // grantResult.AccessTokenString is the access token JWT // grantResult.RefreshTokenString is the refresh token JWT // grantResult.GetAccessTokenAsync() will retrieve the resource as an IAccessToken

In a web application, you can use the Set-Cookie HTTP header to pass the access and refresh token JWTs back to the user’s browser, or return them in a JSON response. (See Where to Store your JWTs – Cookies vs HTML5 Web Storage.)

Verify Access Tokens

When a user makes a request to your application with an access token, it must be validated. The Stormpath .NET SDK supports two validation modes: local and remote.

Local validation can be performed without making a network request, and ensures the following:

Token hasn’t been tampered with

Token hasn’t expired

Issuer is Stormpath

Remote validation makes a request to the Stormpath API, and ensures all of the above plus:

Token hasn’t been revoked

Account hasn’t been disabled, and hasn’t been deleted

Issuing application is still enabled, and hasn’t been deleted

Account is still in an account store for the issuing application

The choice of using local or remote validation depends on the needs of your application.

Remote validation looks like this:

// Build the validation request var jwtAuthenticationRequest = OauthRequests.NewJwtAuthenticationRequest() .SetJwt(accessTokenJwtString) .Build(); // If the request is successful, an IAccessToken is returned. // If the token is invalid, expired, revoked, or tampered with, // a ResourceException is thrown. IAccessToken validAccessToken = await myApp.NewJwtAuthenticator() .AuthenticateAsync(jwtAuthenticationRequest); 1 2 3 4 5 6 7 8 9 10 11 // Build the validation request var jwtAuthenticationRequest = OauthRequests . NewJwtAuthenticationRequest ( ) . SetJwt ( accessTokenJwtString ) . Build ( ) ; // If the request is successful, an IAccessToken is returned. // If the token is invalid, expired, revoked, or tampered with, // a ResourceException is thrown. IAccessToken validAccessToken = await myApp . NewJwtAuthenticator ( ) . AuthenticateAsync ( jwtAuthenticationRequest ) ;

Local validation is identical, except for a call to WithLocalValidation :

// If the request is successful, an IAccessToken is returned. // If the token is invalid, expired, revoked, or tampered with, // the appropriate exception derived from InvalidJwtException is thrown. IAccessToken validAccessToken = await myApp.NewJwtAuthenticator() .WithLocalValidation() .AuthenticateAsync(jwtAuthenticationRequest); 1 2 3 4 5 6 7 // If the request is successful, an IAccessToken is returned. // If the token is invalid, expired, revoked, or tampered with, // the appropriate exception derived from InvalidJwtException is thrown. IAccessToken validAccessToken = await myApp . NewJwtAuthenticator ( ) . WithLocalValidation ( ) . AuthenticateAsync ( jwtAuthenticationRequest ) ;

Refresh Access Tokens

When an access token expires, it’s straightforward to issue a Refresh Grant request to obtain a fresh token:

// Build the Refresh Grant request var refreshGrantRequest = OauthRequests.NewRefreshGrantRequest() .SetRefreshToken(refreshTokenJwtString) // the refresh token JWT .Build(); // Send the request to Stormpath var refreshGrantResult = await myApp.NewRefreshGrantAuthenticator() .AuthenticateAsync(refreshGrantRequest); // refreshGrantResult.AccessTokenHref is the location of the created Access Token resource in Stormpath // refreshGrantResult.ExpiresIn is the number of seconds the access token is valid // refreshGrantResult.AccessTokenString is the new access token JWT // refreshGrantResult.RefreshTokenString is the refresh token JWT // refreshGrantResult.GetAccessTokenAsync() will retrieve the resource as an IAccessToken 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 // Build the Refresh Grant request var refreshGrantRequest = OauthRequests . NewRefreshGrantRequest ( ) . SetRefreshToken ( refreshTokenJwtString ) // the refresh token JWT . Build ( ) ; // Send the request to Stormpath var refreshGrantResult = await myApp . NewRefreshGrantAuthenticator ( ) . AuthenticateAsync ( refreshGrantRequest ) ; // refreshGrantResult.AccessTokenHref is the location of the created Access Token resource in Stormpath // refreshGrantResult.ExpiresIn is the number of seconds the access token is valid // refreshGrantResult.AccessTokenString is the new access token JWT // refreshGrantResult.RefreshTokenString is the refresh token JWT // refreshGrantResult.GetAccessTokenAsync() will retrieve the resource as an IAccessToken

Generating a new access token does not modify the expiration time of the refresh token. Once the refresh token expires, the user must authenticate again to get new access and refresh tokens.

List Tokens for an Account

You can query or list an account’s access and refresh tokens via the appropriate collection resource:

var accessTokens = await account.GetAccessTokens().ToListAsync(); var refreshTokens = await account.GetRefreshTokens().ToListAsync(); 1 2 3 var accessTokens = await account . GetAccessTokens ( ) . ToListAsync ( ) ; var refreshTokens = await account . GetRefreshTokens ( ) . ToListAsync ( ) ;

It’s also possible to restrict the query to a particular Application, given the Application’s href :

var accessTokenForApplication = await account .GetAccessTokens() .Where(x => x.ApplicationHref == myApp.Href) .SingleOrDefaultAsync(); 1 2 3 4 5 var accessTokenForApplication = await account . GetAccessTokens ( ) . Where ( x = > x . ApplicationHref == myApp . Href ) . SingleOrDefaultAsync ( ) ;

Revoke Access and Refresh Tokens

Revoking the access and refresh tokens for an account is necessary in some situations, such as:

The user has explicitly logged out

The user’s device or client has been compromised, and you need to force them to re-authenticate

Revoking tokens is a simple manner of deleting the appropriate resource:

// Get a reference to an IAccessToken or IRefreshToken by looking it up by href, // or by querying the GetAccessTokens() or GetRefreshTokens() collections. await token.DeleteAsync(); 1 2 3 4 5 // Get a reference to an IAccessToken or IRefreshToken by looking it up by href, // or by querying the GetAccessTokens() or GetRefreshTokens() collections. await token . DeleteAsync ( ) ;

Create and Parse JSON Web Tokens (JWTs) in .NET

At the heart of token authentication is the JSON Web Token, which is a standard and compact way of storing identity and claims as a string, with an optional cryptographic signature to prevent tampering. Access and refresh token strings are JWTs that have been Base64-encoded to make them URL-safe.

This SDK release includes built-in support for constructing, signing, parsing, and validating JWTs that use the HMAC SHA-256 (HS256) algorithm.

Construct and Sign JWTs

The IClient interface exposes a builder that can construct JWTs:

var builder = client.NewJwtBuilder(); // IJwtBuilder supports setting any standard JWT claim, // plus arbitrary claims that you define: builder // Set the Audience (aud) claim .SetAudience("Darth Vader") // Set the Expiration (exp) claim .SetExpiration(DateTimeOffset.Now.AddDays(10)) // Set the JWT ID (jti) claim .SetId($"jwt-id-{Guid.NewGuid()}") // Set the Issued-At (iat) claim .SetIssuedAt(DateTimeOffset.Now) // Set the Issuer (iss) claim .SetIssuer("Lord Sidious") // Set the Subject (sub) claim: .SetSubject("Secret Plans") // SetClaim() can be used to add any claim as a key-value pair: .SetClaim("title", "Death Star") // SignWith() is used to sign the JWT with a secret key: .SignWith("my_secret_key_123", Encoding.UTF8); // Build the JWT var jwt = builder.Build(); 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 var builder = client . NewJwtBuilder ( ) ; // IJwtBuilder supports setting any standard JWT claim, // plus arbitrary claims that you define: builder // Set the Audience (aud) claim . SetAudience ( "Darth Vader" ) // Set the Expiration (exp) claim . SetExpiration ( DateTimeOffset . Now . AddDays ( 10 ) ) // Set the JWT ID (jti) claim . SetId ( $ "jwt-id-{Guid.NewGuid()}" ) // Set the Issued-At (iat) claim . SetIssuedAt ( DateTimeOffset . Now ) // Set the Issuer (iss) claim . SetIssuer ( "Lord Sidious" ) // Set the Subject (sub) claim: . SetSubject ( "Secret Plans" ) // SetClaim() can be used to add any claim as a key-value pair: . SetClaim ( "title" , "Death Star" ) // SignWith() is used to sign the JWT with a secret key: . SignWith ( "my_secret_key_123" , Encoding . UTF8 ) ; // Build the JWT var jwt = builder . Build ( ) ;

The IJwt interface represents a constructed or parsed JWT, and includes a deserialized body (payload):

// jwt.Base64Header, jwt.Base64Payload, and jwt.Base64Digest // contain the original base64 parts of the JWT string base64Digest = jwt.Base64Digest; // The header is represented as a dictionary var alg = jwt.Header["alg"]; // The deserialized body is available via Body string aud = jwt.Body.Audience; DateTimeOffset exp = jwt.Body.Expiration.Value; // IJwtClaims includes some helper methods for working with claims bool containsTitle = jwt.Body.ContainsClaim("title"); // true string title = jwt.Body.GetClaim("title").ToString(); // Get a string representation of the entire JWT string token = jwt.ToString(); 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 // jwt.Base64Header, jwt.Base64Payload, and jwt.Base64Digest // contain the original base64 parts of the JWT string base64Digest = jwt . Base64Digest ; // The header is represented as a dictionary var alg = jwt . Header [ "alg" ] ; // The deserialized body is available via Body string aud = jwt . Body . Audience ; DateTimeOffset exp = jwt . Body . Expiration . Value ; // IJwtClaims includes some helper methods for working with claims bool containsTitle = jwt . Body . ContainsClaim ( "title" ) ; // true string title = jwt . Body . GetClaim ( "title" ) . ToString ( ) ; // Get a string representation of the entire JWT string token = jwt . ToString ( ) ;

The result of IJwt.ToString() is Base64-encoded and guaranteed to be URL-safe.

Likewise, IClient also exposes a JWT parser and validator:

var parser = client.NewJwtParser(); // Parse and validate a JWT var parsedJwt = parser // Sets the secret key used to sign the JWT .SetSigningKey("my_secret_key_123", Encoding.UTF8) // Verifies and deserializes the JWT .Parse(token); 1 2 3 4 5 6 7 8 9 var parser = client . NewJwtParser ( ) ; // Parse and validate a JWT var parsedJwt = parser // Sets the secret key used to sign the JWT . SetSigningKey ( "my_secret_key_123" , Encoding . UTF8 ) // Verifies and deserializes the JWT . Parse ( token ) ;

If validation fails, the appropriate exception derived from InvalidJwtException will be thrown. By default, the signature and lifetime (Expiration and Not-Before claims) are validated when parsing any JWT. You can add additional validation requirements with the Require methods:

var parsedJwt = parser // If these claims do not exist, a MissingClaimException is thrown // If the claim exists but the value does not match, a MismatchedClaimException is thrown .RequireAudience("Darth Vader") .RequireIssuer("Lord Sidious") .RequireClaim("title", "Death Star") .SetSigningKey("my_secret_key_123", Encoding.UTF8) // Verifies and deserializes the JWT .Parse(token); 1 2 3 4 5 6 7 8 9 10 var parsedJwt = parser // If these claims do not exist, a MissingClaimException is thrown // If the claim exists but the value does not match, a MismatchedClaimException is thrown . RequireAudience ( "Darth Vader" ) . RequireIssuer ( "Lord Sidious" ) . RequireClaim ( "title" , "Death Star" ) . SetSigningKey ( "my_secret_key_123" , Encoding . UTF8 ) // Verifies and deserializes the JWT . Parse ( token ) ;

That’s all there is to it!

Token-based authentication and JSON Web Tokens are powerful tools for building modern web and mobile applications that are secure and scalable. The Stormpath .NET SDK simplifies the process of creating and managing tokens, so you can focus your effort on building your application.

What’s Next for the .NET SDK?

There’s plenty more to come! Here’s what’s on the roadmap:

Integration with ASP.NET

SAML support

Is there something specific you want to see? Report bugs and feature requests on Github or shoot us an email.