Skip to content

Commit

Permalink
Adds support for JWT-secured OAuth 2.0 authorisation request (JAR) (#17)
Browse files Browse the repository at this point in the history
  • Loading branch information
gunndabad authored Apr 23, 2024
1 parent d58646e commit f2f186e
Show file tree
Hide file tree
Showing 3 changed files with 102 additions and 18 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## Unreleased

Adds support for JWT-secured OAuth 2.0 authorisation request (JAR) and enables it by default.

## 0.3.1

Adds `NationalInsuranceNumber` member to `OneLoginClaimTypes`.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
using System.Text.Json;
using System.Text.Json.Nodes;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;

namespace GovUk.OneLogin.AspNetCore;

internal delegate string CreateJwtSecuredAuthorizationRequest(IDictionary<string, object> claims);

internal class JwtSecuredAuthorizationRequestMessage : OpenIdConnectMessage
{
private readonly CreateJwtSecuredAuthorizationRequest _createJwt;

public JwtSecuredAuthorizationRequestMessage(OpenIdConnectMessage message, CreateJwtSecuredAuthorizationRequest createJwt) : base(message)
{
ArgumentNullException.ThrowIfNull(createJwt);
_createJwt = createJwt;
}

public override string CreateAuthenticationRequestUrl()
{
// https://openid.net/specs/openid-connect-core-1_0.html#RequestObject

var openIdConnectMessage = Clone();
openIdConnectMessage.RequestType = OpenIdConnectRequestType.Authentication;

Dictionary<string, object> claims = openIdConnectMessage.Parameters
.ToDictionary(
kvp => kvp.Key,
kvp =>
{
if (kvp.Key is "vtr" or "claims")
{
return JsonSerializer.SerializeToElement(JsonNode.Parse(kvp.Value));
}
else
{
return (object)kvp.Value;
}
});
claims.Add("iss", ClientId);

foreach (var key in openIdConnectMessage.Parameters.Keys)
{
if (key is not "response_type" and not "scope" and not "client_id")
{
openIdConnectMessage.RemoveParameter(key);
}
}

var request = _createJwt(claims);
openIdConnectMessage.SetParameter("request", request);

return openIdConnectMessage.BuildRedirectUrl();
}
}
61 changes: 43 additions & 18 deletions src/GovUk.OneLogin.AspNetCore/OneLoginOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ public OneLoginOptions()
Scope.Clear();
Scope.Add("openid");
Scope.Add("email");

UseJwtSecuredAuthorizationRequest = true;
}

/// <inheritdoc cref="OpenIdConnectOptions.MetadataAddress"/>
Expand Down Expand Up @@ -156,6 +158,12 @@ public CookieBuilder CorrelationCookie
/// <inheritdoc cref="OpenIdConnectOptions.Events"/>
public OpenIdConnectEvents Events { get; }

/// <summary>
/// Defines whether the authorization parameters will be passed as a JWT.
/// This property is set to <see langword="true"/> by default.
/// </summary>
public bool UseJwtSecuredAuthorizationRequest { get; set; }

internal OpenIdConnectOptions OpenIdConnectOptions { get; private set; }

internal bool IncludesCoreIdentityClaim => Claims.Contains(OneLoginClaimTypes.CoreIdentity);
Expand Down Expand Up @@ -199,7 +207,7 @@ internal Task OnAuthorizationCodeReceived(AuthorizationCodeReceivedContext conte
return Task.CompletedTask;
}

internal Task OnRedirectToIdentityProvider(RedirectContext context)
internal async Task OnRedirectToIdentityProvider(RedirectContext context)
{
var vectorOfTrust = (context.Properties.TryGetVectorOfTrust(out var value) ? value : VectorOfTrust) ??
throw new InvalidOperationException(
Expand All @@ -218,7 +226,18 @@ internal Task OnRedirectToIdentityProvider(RedirectContext context)
context.ProtocolMessage.Parameters.Add("ui_locales", UiLocales);
}

return Task.CompletedTask;
if (UseJwtSecuredAuthorizationRequest)
{
var configuration = await OpenIdConnectOptions.ConfigurationManager!.GetConfigurationAsync(CancellationToken.None);

context.ProtocolMessage = new JwtSecuredAuthorizationRequestMessage(
context.ProtocolMessage,
createJwt: claims =>
{
claims.Add("aud", configuration.AuthorizationEndpoint);
return CreateJwt(claims, handler => handler.SetDefaultTimesOnTokenCreation = false);
});
}
}

internal Task OnTokenResponseReceived(TokenResponseReceivedContext context)
Expand All @@ -238,32 +257,38 @@ internal Task OnTokenResponseReceived(TokenResponseReceivedContext context)
return Task.CompletedTask;
}

private string CreateJwt(IDictionary<string, object> claims, Action<JsonWebTokenHandler>? configureHandler = null)
{
var handler = new JsonWebTokenHandler();
configureHandler?.Invoke(handler);

var tokenDescriptor = new SecurityTokenDescriptor()
{
Claims = claims,
SigningCredentials = ClientAuthenticationCredentials
};

return handler.CreateToken(tokenDescriptor);
}

private string CreateClientAssertionJwt()
{
// https://docs.sign-in.service.gov.uk/integrate-with-integration-environment/integrate-with-code-flow/#create-a-jwt

ValidateOptionNotNull(ClientAssertionJwtAudience);
ValidateOptionNotNull(ClientAuthenticationCredentials);

var handler = new JsonWebTokenHandler();

var jwtId = Guid.NewGuid().ToString("N");

var tokenDescriptor = new SecurityTokenDescriptor()
return CreateJwt(new Dictionary<string, object>()
{
Claims = new Dictionary<string, object>()
{
{ "aud", ClientAssertionJwtAudience },
{ "iss", ClientId! },
{ "sub", ClientId! },
{ "exp", DateTimeOffset.UtcNow.Add(ClientAssertionJwtExpiry).ToUnixTimeSeconds() },
{ "jti", jwtId },
{ "iat", DateTimeOffset.UtcNow.ToUnixTimeSeconds() }
},
SigningCredentials = ClientAuthenticationCredentials
};

return handler.CreateToken(tokenDescriptor);
{ "aud", ClientAssertionJwtAudience },
{ "iss", ClientId! },
{ "sub", ClientId! },
{ "exp", DateTimeOffset.UtcNow.Add(ClientAssertionJwtExpiry).ToUnixTimeSeconds() },
{ "jti", jwtId },
{ "iat", DateTimeOffset.UtcNow.ToUnixTimeSeconds() }
});
}

private string GetClaimsRequest()
Expand Down

0 comments on commit f2f186e

Please sign in to comment.