Skip to content
This repository was archived by the owner on Apr 27, 2023. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 29 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Simple Token Provider Middleware for ASP.NET

This project demonstrates how to generate [JSON Web Tokens](https://en.wikipedia.org/wiki/JSON_Web_Token) (JWTs) for token authentication in ASP.NET Core RC2. The functionality is wrapped up in a reusable middleware component.
This project demonstrates how to generate [JSON Web Tokens](https://en.wikipedia.org/wiki/JSON_Web_Token) (JWTs) for token authentication and how to generate a refreshed token in ASP.NET Core RC2. The functionality is wrapped up in a reusable middleware component.

Original blog post: [Token Authentication in ASP.NET Core](https://stormpath.com/blog/token-authentication-asp-net-core)

Expand All @@ -14,6 +14,7 @@ The token provider endpoint can be added to your pipeline in `Configure()`:
app.UseSimpleTokenProvider(new TokenProviderOptions
{
Path = "/api/token",
RefreshPath = "api/refresh-token",
Audience = "ExampleAudience",
Issuer = "ExampleIssuer",
SigningCredentials = signingCredentials,
Expand All @@ -24,6 +25,7 @@ app.UseSimpleTokenProvider(new TokenProviderOptions
The options are:

* **Path** (optional) - The endpoint path relative to the server root. Default: `/token`
* **RefreshPath** (optional) - The endpoint path relative to the server root used for token refresh. Default: `/refresh-token`
* **Audience** - The JWT `aud` claim value.
* **Issuer** - The JWT `iss` claim value.
* **Expiration** (optional) - The expiration duration for new tokens. Default: 5 minutes
Expand Down Expand Up @@ -65,6 +67,7 @@ private Task<ClaimsIdentity> GetIdentity(string username, string password)

At a high level, the middleware does the following:

### Create
* Intercepts requests to `options.Path`
* Verifies the request is a POST with `Content-Type: application/x-www-form-urlencoded`
* Pulls the username and password out of the form body
Expand All @@ -79,6 +82,13 @@ At a high level, the middleware does the following:
* `aud` (audience) - `options.Audience`
* Encodes the JWT to a string and sends it back to the client

### Refresh
* Intercepts requests to `options.RefreshPath`
* Verifies the request is a POST with `Content-Type: application/x-www-form-urlencoded` (probably not required)
* Validates passed JWT.
* Based on passed token it creates a new one with new expiration date.
* Encodes the new JWT to a string and sends it back to the client

## Trying it out

You can install the middleware in a new project, or just run the included test project. Send a POST request using a tool like Fiddler or Postman:
Expand All @@ -99,6 +109,24 @@ You should get a `200 OK` response:
}
```

And for refresh:

```
POST /refresh-token (or whatever you set options.RefreshPath to)
Content-Type: application/x-www-form-urlencoded
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJURVNUIiwianRpIjoiYzRjYzdhMmUtMjI0OS00ZWUzLWJkM2MtYzU5MDkzYmU5MGU1IiwiaWF0IjoxNDYzNTMwMDI0LCJuYmYiOjE0NjM1MzAwMjMsImV4cCI6MTQ2MzUzMDMyMywiaXNzIjoiRXhhbXBsZUlzc3VlciIsImF1ZCI6IkV4YW1wbGVBdWRpZW5jZSJ9.mI0NPO437IuBSt5kmayy5XhNFEHVF4IyMkKsmtas6w8

```

You should get a `200 OK` response:

```
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJURVNUIiwianRpIjoiNjY1NjBlMTctZGRmNy00MWNhLWE1NWMtMjgxNmZjZjU0NzU2IiwiaWF0IjoxNDgwNjcwMjIxLCJuYmYiOjE0ODA2NzAyNzAsImV4cCI6MTQ4MDY3MDU3MCwiaXNzIjoiRXhhbXBsZUlzc3VlciIsImF1ZCI6WyJFeGFtcGxlQXVkaWVuY2UiLCJFeGFtcGxlQXVkaWVuY2UiLCJFeGFtcGxlQXVkaWVuY2UiXX0.9WMoq2V0SE8ECs2Qzk2ymGL-BeYrPJc9_oRkLhdmW2g",
"expires_in": 300
}
```

You can try decoding and verifying the JWT at [jsonwebtoken.io](https://jsonwebtoken.io).

## Acknowledgements
Expand Down
10 changes: 8 additions & 2 deletions src/SimpleTokenProvider/TokenProviderAppBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;

namespace SimpleTokenProvider
{
Expand All @@ -17,7 +18,7 @@ public static class TokenProviderAppBuilderExtensions
/// <param name="app">The <see cref="IApplicationBuilder"/> to add the middleware to.</param>
/// <param name="options">A <see cref="TokenProviderOptions"/> that specifies options for the middleware.</param>
/// <returns>A reference to this instance after the operation has completed.</returns>
public static IApplicationBuilder UseSimpleTokenProvider(this IApplicationBuilder app, TokenProviderOptions options)
public static IApplicationBuilder UseSimpleTokenProvider(this IApplicationBuilder app, TokenProviderOptions options, TokenValidationParameters tokenValidationParameters)
{
if (app == null)
{
Expand All @@ -29,7 +30,12 @@ public static IApplicationBuilder UseSimpleTokenProvider(this IApplicationBuilde
throw new ArgumentNullException(nameof(options));
}

return app.UseMiddleware<TokenProviderMiddleware>(Options.Create(options));
if (tokenValidationParameters == null)
{
throw new ArgumentNullException(nameof(tokenValidationParameters));
}

return app.UseMiddleware<TokenProviderMiddleware>(Options.Create(options), tokenValidationParameters);
}
}
}
60 changes: 57 additions & 3 deletions src/SimpleTokenProvider/TokenProviderMiddleware.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,18 +23,25 @@ public class TokenProviderMiddleware
{
private readonly RequestDelegate _next;
private readonly TokenProviderOptions _options;
private readonly TokenValidationParameters _tokenValidationParameters;
private readonly ILogger _logger;
private readonly JsonSerializerSettings _serializerSettings;

public TokenProviderMiddleware(
RequestDelegate next,
IOptions<TokenProviderOptions> options,
TokenValidationParameters tokenValidationParameters,
ILoggerFactory loggerFactory)
{
_next = next;
_logger = loggerFactory.CreateLogger<TokenProviderMiddleware>();
_tokenValidationParameters = tokenValidationParameters;

_options = options.Value;
if(tokenValidationParameters == null)
{
throw new ArgumentNullException(nameof(tokenValidationParameters));
}
ThrowIfInvalidOptions(_options);

_serializerSettings = new JsonSerializerSettings
Expand All @@ -45,8 +52,11 @@ public TokenProviderMiddleware(

public Task Invoke(HttpContext context)
{
// first figure out whether this is a request for a new token or for a refresh
bool isCreate = context.Request.Path.Equals(_options.Path, StringComparison.Ordinal);
bool isRefresh = !isCreate && context.Request.Path.Equals(_options.RefreshPath, StringComparison.Ordinal);
// If the request path doesn't match, skip
if (!context.Request.Path.Equals(_options.Path, StringComparison.Ordinal))
if (!isCreate && !isRefresh)
{
return _next(context);
}
Expand All @@ -59,9 +69,48 @@ public Task Invoke(HttpContext context)
return context.Response.WriteAsync("Bad request.");
}

_logger.LogInformation("Handling request: " + context.Request.Path);
_logger.LogInformation($"Handling request for {(isCreate ? "create token": "refresh token")}: " + context.Request.Path);

return GenerateToken(context);
if (isCreate)
{
return GenerateToken(context);
}
else
{
return IssueRefreshedToken(context);
}
}

private Task IssueRefreshedToken(HttpContext context)
{
try
{
// first extract token text from Authorization header
string authenticationText = context.Request.Headers["Authorization"].ToString();
int firstSpace = authenticationText.IndexOf(" ");
string tokenText = authenticationText.Substring(firstSpace + 1);

var jwtSecurityTokenHandler = new JwtSecurityTokenHandler();
SecurityToken originalToken;
// validate token using validation parameters
var claimsi = jwtSecurityTokenHandler.ValidateToken(tokenText, _tokenValidationParameters, out originalToken);
var now = DateTime.UtcNow;
// create a new token based on original one
// apply new expiration
var jwt = new JwtSecurityToken(
issuer: _options.Issuer,
audience: _options.Audience,
claims: ((JwtSecurityToken)originalToken).Claims,
notBefore: now,
expires: now.Add(_options.Expiration),
signingCredentials: _options.SigningCredentials);
return WriteTokenResponse(context, jwt);
}
catch
{
context.Response.StatusCode = 400;
return context.Response.WriteAsync("Bad request or invalid token.");
}
}

private async Task GenerateToken(HttpContext context)
Expand Down Expand Up @@ -96,6 +145,11 @@ private async Task GenerateToken(HttpContext context)
notBefore: now,
expires: now.Add(_options.Expiration),
signingCredentials: _options.SigningCredentials);
await WriteTokenResponse(context, jwt);
}

private async Task WriteTokenResponse(HttpContext context, JwtSecurityToken jwt)
{
var encodedJwt = new JwtSecurityTokenHandler().WriteToken(jwt);

var response = new
Expand Down
5 changes: 5 additions & 0 deletions src/SimpleTokenProvider/TokenProviderOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ public class TokenProviderOptions
/// </summary>
/// <remarks>The default path is <c>/token</c>.</remarks>
public string Path { get; set; } = "/token";
/// <summary>
/// The relative path for refresh token.
/// </summary>
/// <remarks>The default path is <c>/refresh-token</c></remarks>
public string RefreshPath { get; set; } = "/refresh-token";

/// <summary>
/// The Issuer (iss) claim for generated tokens.
Expand Down
22 changes: 11 additions & 11 deletions test/SimpleTokenProvider.Test/Startup.Auth.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,6 @@ public partial class Startup
private void ConfigureAuth(IApplicationBuilder app)
{
var signingKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(secretKey));

app.UseSimpleTokenProvider(new TokenProviderOptions
{
Path = "/api/token",
Audience = "ExampleAudience",
Issuer = "ExampleIssuer",
SigningCredentials = new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256),
IdentityResolver = GetIdentity
});

var tokenValidationParameters = new TokenValidationParameters
{
// The signing key must match!
Expand All @@ -43,11 +33,21 @@ private void ConfigureAuth(IApplicationBuilder app)

// Validate the token expiry
ValidateLifetime = true,

// If you want to allow a certain amount of clock drift, set that here:
ClockSkew = TimeSpan.Zero
};

app.UseSimpleTokenProvider(new TokenProviderOptions
{
Path = "/api/token",
RefreshPath = "/api/refresh-token",
Audience = "ExampleAudience",
Issuer = "ExampleIssuer",
SigningCredentials = new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256),
IdentityResolver = GetIdentity
}, tokenValidationParameters);

app.UseJwtBearerAuthentication(new JwtBearerOptions
{
AutomaticAuthenticate = true,
Expand Down