Skip to content
Merged
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
3 changes: 3 additions & 0 deletions docs/reference/options.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ IdentityServer Options
* ``AccessTokenJwtType``
Specifies the value used for the JWT typ header for access tokens (defaults to ``at+jwt``).

* ``LogoutTokenJwtType``
Specifies the value used for the JWT typ header for back-channel logout tokens (defaults to ``logout+jwt``). ``null`` or empty will use ``JWT``.

* ``EmitScopesAsSpaceDelimitedStringInJwt``
Specifies whether scopes in JWTs are emitted as array or string

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,14 @@ public class IdentityServerOptions
/// </value>
public string AccessTokenJwtType { get; set; } = "at+jwt";

/// <summary>
/// Gets or sets the value for the JWT typ header for logout tokens.
/// </summary>
/// <value>
/// The JWT typ value.
/// </value>
public string LogoutTokenJwtType { get; set; } = "logout+jwt";

/// <summary>
/// Emits an aud claim with the format issuer/resources. That's needed for some older access token validation plumbing. Defaults to false.
/// </summary>
Expand Down
1 change: 1 addition & 0 deletions src/Open.IdentityServer/src/IdentityServerConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ public static class TokenTypes
{
public const string IdentityToken = "id_token";
public const string AccessToken = "access_token";
public const string LogoutToken = "logout_token";
}

public static class ClaimValueTypes
Expand Down
17 changes: 17 additions & 0 deletions src/Open.IdentityServer/src/IdentityServerTools.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,22 @@ public virtual async Task<string> IssueJwtAsync(int lifetime, IEnumerable<Claim>
/// </returns>
/// <exception cref="System.ArgumentNullException">claims</exception>
public virtual async Task<string> IssueJwtAsync(int lifetime, string issuer, IEnumerable<Claim> claims)
{
return await IssueJwtAsync(lifetime, issuer, IdentityServerConstants.TokenTypes.AccessToken, claims);
}

/// <summary>
/// Issues a JWT.
/// </summary>
/// <param name="lifetime">The lifetime of the JWT in seconds.</param>
/// <param name="issuer">The issuer to include in the JWT.</param>
/// <param name="tokenType">The token type to include in the JWT.</param>
/// <param name="claims">The claims to include in the JWT.</param>
/// <returns>
/// A task that resolves to the signed JWT string.
/// </returns>
/// <exception cref="System.ArgumentNullException">claims</exception>
public virtual async Task<string> IssueJwtAsync(int lifetime, string issuer, string tokenType, IEnumerable<Claim> claims)
{
if (String.IsNullOrWhiteSpace(issuer)) throw new ArgumentNullException(nameof(issuer));
if (claims == null) throw new ArgumentNullException(nameof(claims));
Expand All @@ -75,6 +91,7 @@ public virtual async Task<string> IssueJwtAsync(int lifetime, string issuer, IEn
{
CreationTime = _clock.GetUtcNow().UtcDateTime,
Issuer = issuer,
Type = tokenType,
Lifetime = lifetime,
Claims = new HashSet<Claim>(claims, new ClaimComparer()),
Audiences = audiences
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ protected virtual async Task<string> CreateTokenAsync(BackChannelLogoutRequest r
}

var issuer = request.Issuer ?? Tools.ContextAccessor.HttpContext.GetIdentityServerIssuerUri();
return await Tools.IssueJwtAsync(DefaultLogoutTokenLifetime, issuer, claims);
return await Tools.IssueJwtAsync(DefaultLogoutTokenLifetime, issuer, IdentityServerConstants.TokenTypes.LogoutToken, claims);
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,13 @@ protected virtual async Task<JwtHeader> CreateHeaderAsync(Token token)
header["typ"] = Options.AccessTokenJwtType;
}
}
else if (token.Type == TokenTypes.LogoutToken)
{
if (Options.LogoutTokenJwtType.IsPresent())
{
header["typ"] = Options.LogoutTokenJwtType;
}
}

return header;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -577,6 +577,34 @@ await _mockPipeline.BrowserClient.GetAsync(
_mockPipeline.BackChannelMessageHandler.InvokeWasCalled.Should().BeTrue();
}

[Fact]
[Trait("Category", Category)]
public async Task back_channel_logout_token_should_have_logout_jwt_typ()
{
// assert outside OnInvoke: BackChannelLogoutHttpClient swallows exceptions thrown by the handler
string form = null;
_mockPipeline.BackChannelMessageHandler.OnInvoke = async req => form = await req.Content.ReadAsStringAsync();

await _mockPipeline.LoginAsync("bob");

await _mockPipeline.RequestAuthorizationEndpointAsync(
clientId: "client3",
responseType: "id_token",
scope: "openid",
redirectUri: "https://client3/callback",
state: "123_state",
nonce: "123_nonce");

await _mockPipeline.BrowserClient.GetAsync(
IdentityServerPipeline.EndSessionEndpoint,
TestContext.Current.CancellationToken);

form.Should().StartWith(OidcConstants.BackChannelLogoutRequest.LogoutToken + "=");
var token = form.Split('=')[1];
var header = JsonElement.Parse(Encoding.UTF8.GetString(Base64Url.Decode(token.Split('.')[0])));
header.GetProperty("typ").GetString().Should().Be("logout+jwt");
}

[Fact]
[Trait("Category", Category)]
public async Task back_channel_logout_should_not_affect_end_session_callback()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,4 +66,30 @@ public async Task IssueJwtAsync_WhenCalledWithMultipleClaimsContainingAudience_S
t.Audiences.Contains(audience3))),
Times.Once);
}

[Fact]
public async Task IssueJwtAsync_WhenCalledWithTokenType_ShouldSetTypeOnToken()
{
var sut = CreateSut();

await sut.IssueJwtAsync(300, "https://localhost:5000", IdentityServerConstants.TokenTypes.LogoutToken,
new List<Claim> { new(JwtClaimTypes.Subject, "bob") });

_tokenCreationService.Verify(x => x.CreateTokenAsync(It.Is<Token>(t =>
t.Type == IdentityServerConstants.TokenTypes.LogoutToken)),
Times.Once);
}

[Fact]
public async Task IssueJwtAsync_WhenCalledWithoutTokenType_ShouldIssueAccessToken()
{
var sut = CreateSut();

await sut.IssueJwtAsync(3600, "https://localhost:5000",
new List<Claim> { new(JwtClaimTypes.Subject, "bob") });

_tokenCreationService.Verify(x => x.CreateTokenAsync(It.Is<Token>(t =>
t.Type == IdentityServerConstants.TokenTypes.AccessToken)),
Times.Once);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@

using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Cryptography;
using System.Threading.Tasks;
using AwesomeAssertions;
using Microsoft.IdentityModel.Tokens;
using Moq;
using Open.IdentityServer.Configuration;
Expand Down Expand Up @@ -48,6 +50,23 @@ private DefaultTokenCreationService CreateSubject()
TestLogger.Create<DefaultTokenCreationService>());
}

private Token CreateToken(string type)
{
var now = DateTime.UtcNow;

_clock.Setup(c => c.GetUtcNow()).Returns(now);

return new Token
{
Type = type,
CreationTime = now,
Lifetime = 60,
Issuer = "https://issuer.test"
};
}

private static string ReadTyp(string jwt) => new JwtSecurityTokenHandler().ReadJwtToken(jwt).Header.Typ;

[Fact]
public async Task CreateTokenAsync_WhenCalled_ShouldInitiateTelemetryTrace()
{
Expand Down Expand Up @@ -77,4 +96,56 @@ public async Task CreateTokenAsync_WhenCalled_ShouldInitiateTelemetryTrace()
"CreateTokenAsync"));
_trace.Verify(t => t.Dispose(), Times.Once);
}

[Fact]
public async Task CreateTokenAsync_WhenTokenIsLogoutToken_ShouldSetTypToLogoutJwt()
{
var token = CreateToken(IdentityServerConstants.TokenTypes.LogoutToken);

var subject = CreateSubject();

var jwt = await subject.CreateTokenAsync(token);

ReadTyp(jwt).Should().Be("logout+jwt");
}

[Fact]
public async Task CreateTokenAsync_WhenLogoutTokenJwtTypeIsConfigured_ShouldUseConfiguredTyp()
{
_options.LogoutTokenJwtType = "at+jwt";
var token = CreateToken(IdentityServerConstants.TokenTypes.LogoutToken);

var subject = CreateSubject();

var jwt = await subject.CreateTokenAsync(token);

ReadTyp(jwt).Should().Be("at+jwt");
}

[Theory]
[InlineData(null)]
[InlineData("")]
public async Task CreateTokenAsync_WhenLogoutTokenJwtTypeIsEmpty_ShouldFallBackToJwt(string logoutTokenJwtType)
{
_options.LogoutTokenJwtType = logoutTokenJwtType;
var token = CreateToken(IdentityServerConstants.TokenTypes.LogoutToken);

var subject = CreateSubject();

var jwt = await subject.CreateTokenAsync(token);

ReadTyp(jwt).Should().Be("JWT");
}

[Fact]
public async Task CreateTokenAsync_WhenTokenIsAccessToken_ShouldKeepAccessTokenTyp()
{
var token = CreateToken(OidcConstants.TokenTypes.AccessToken);

var subject = CreateSubject();

var jwt = await subject.CreateTokenAsync(token);

ReadTyp(jwt).Should().Be("at+jwt");
}
}
Loading