Skip to content

Commit

Permalink
Merge pull request #1403 from DuendeSoftware/joe/refresh-token-thumpr…
Browse files Browse the repository at this point in the history
…ints

Joe/refresh token thumprints
  • Loading branch information
brockallen authored Sep 7, 2023
2 parents d7cdd3c + 5314d8f commit 1e7db6b
Show file tree
Hide file tree
Showing 5 changed files with 136 additions and 4 deletions.
20 changes: 20 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -516,6 +516,26 @@
"group": "20-clients",
}
},
{
"name": "client: MvcDPoP",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build-client-MvcDPoP",
"program": "${workspaceFolder}/clients/src/MvcDPoP/bin/Debug/net8.0/MvcDPoP.dll",
"args": [],
"cwd": "${workspaceFolder}/clients/src/MvcDPoP",
"serverReadyAction": {
"action": "openExternally",
"pattern": "\\bNow listening on:\\s+(https?://\\S+)"
},
"env": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"presentation": {
"hidden": false,
"group": "20-clients",
}
},
{
"name": "client: MvcHybridBackChannel",
"type": "coreclr",
Expand Down
12 changes: 12 additions & 0 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,18 @@
],
"problemMatcher": "$msCompile"
},
{
"label": "build-client-MvcDPoP",
"type": "process",
"command": "dotnet",
"args": [
"build",
"${workspaceFolder}/clients/src/MvcDPoP/MvcDPoP.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],
"problemMatcher": "$msCompile"
},
{
"label": "build-client-MvcHybridBackChannel",
"type": "process",
Expand Down
6 changes: 3 additions & 3 deletions src/IdentityServer/Extensions/TokenExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -180,11 +180,11 @@ private static ProofKeyThumbprint GetProofKeyThumbprint(string cnf)
{
if (cnf.IsPresent())
{
var data = JsonSerializer.Deserialize<Dictionary<string, object>>(cnf);
var data = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(cnf);

if (data.TryGetValue(JwtClaimTypes.ConfirmationMethods.JwkThumbprint, out var jkt))
{
var thumbprint = jkt as string;
var thumbprint = jkt.ToString();
if (thumbprint.IsPresent())
{
return new ProofKeyThumbprint { Type = ProofType.DPoP, Thumbprint = thumbprint };
Expand All @@ -193,7 +193,7 @@ private static ProofKeyThumbprint GetProofKeyThumbprint(string cnf)

if (data.TryGetValue("x5t#S256", out var x5t))
{
var thumbprint = x5t as string;
var thumbprint = x5t.ToString();
if (thumbprint.IsPresent())
{
return new ProofKeyThumbprint { Type = ProofType.ClientCertificate, Thumbprint = thumbprint };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,9 @@ public DPoPTokenEndpointTests()
RedirectUris = { "https://client1/callback" },
RequirePkce = false,
AllowOfflineAccess = true,
RefreshTokenUsage = TokenUsage.ReUse,
AllowedScopes = new List<string> { "openid", "profile", "scope1" },
},
}
});

_mockPipeline.Users.Add(new TestUser
Expand Down Expand Up @@ -689,6 +690,78 @@ public async Task public_client_should_not_be_able_to_use_different_dpop_key_for
rtResponse.Error.Should().Be("invalid_dpop_proof");
}

[Fact]
[Trait("Category", Category)]
public async Task public_client_using_same_dpop_key_for_refresh_token_request_should_succeed()
{
_dpopClient.RequireClientSecret = false;

await _mockPipeline.LoginAsync("bob");

_mockPipeline.BrowserClient.AllowAutoRedirect = false;

var url = _mockPipeline.CreateAuthorizeUrl(
clientId: "client1",
responseType: "code",
responseMode: "query",
scope: "openid scope1 offline_access",
redirectUri: "https://client1/callback");
var response = await _mockPipeline.BrowserClient.GetAsync(url);

response.StatusCode.Should().Be(HttpStatusCode.Redirect);
response.Headers.Location.ToString().Should().StartWith("https://client1/callback");

var authorization = new AuthorizeResponse(response.Headers.Location.ToString());
authorization.IsError.Should().BeFalse();

var codeRequest = new AuthorizationCodeTokenRequest
{
Address = IdentityServerPipeline.TokenEndpoint,
ClientId = "client1",
ClientSecret = "secret",
Code = authorization.Code,
RedirectUri = "https://client1/callback",
};
codeRequest.Headers.Add("DPoP", CreateDPoPProofToken());

var codeResponse = await _mockPipeline.BackChannelClient.RequestAuthorizationCodeTokenAsync(codeRequest);
codeResponse.IsError.Should().BeFalse();
codeResponse.TokenType.Should().Be("DPoP");
GetJKTFromAccessToken(codeResponse).Should().Be(_JKT);

var firstRefreshRequest = new RefreshTokenRequest
{
Address = IdentityServerPipeline.TokenEndpoint,
ClientId = "client1",
ClientSecret = "secret",
RefreshToken = codeResponse.RefreshToken
};
firstRefreshRequest.Headers.Add("DPoP", CreateDPoPProofToken());

var firstRefreshResponse = await _mockPipeline.BackChannelClient.RequestRefreshTokenAsync(firstRefreshRequest);
firstRefreshResponse.IsError.Should().BeFalse();
firstRefreshResponse.TokenType.Should().Be("DPoP");
GetJKTFromAccessToken(firstRefreshResponse).Should().Be(_JKT);

var secondRefreshRequest = new RefreshTokenRequest
{
Address = IdentityServerPipeline.TokenEndpoint,
ClientId = "client1",
ClientSecret = "secret",
RefreshToken = codeResponse.RefreshToken
};
secondRefreshRequest.Headers.Add("DPoP", CreateDPoPProofToken());

firstRefreshRequest.Headers.GetValues("DPoP").FirstOrDefault().Should().NotBe(
secondRefreshRequest.Headers.GetValues("DPoP").FirstOrDefault());

var secondRefreshResponse = await _mockPipeline.BackChannelClient.RequestRefreshTokenAsync(secondRefreshRequest);
secondRefreshResponse.IsError.Should().BeFalse(secondRefreshResponse.Error);
secondRefreshResponse.TokenType.Should().Be("DPoP");
GetJKTFromAccessToken(secondRefreshResponse).Should().Be(_JKT);
}


[Fact]
[Trait("Category", Category)]
public async Task missing_proof_token_when_required_on_refresh_token_request_should_fail()
Expand Down
27 changes: 27 additions & 0 deletions test/IdentityServer.UnitTests/Extensions/TokenExtensionsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
using Duende.IdentityServer.Models;
using IdentityModel;
using Microsoft.AspNetCore.Authentication;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Text.Json;
using UnitTests.Common;
Expand Down Expand Up @@ -43,4 +45,29 @@ public void TestClaimValueTypes(string type, string value, string valueType, str

Assert.Contains(expected, payloadJson);
}

[Fact]
public void refresh_token_should_get_mtls_x5t_thumprint()
{
var expected = "some hash normally goes here";

var cnf = new Dictionary<string, string>
{
{ "x5t#S256", expected }
};

var refreshToken = new RefreshToken()
{
AccessTokens = new Dictionary<string, Token>
{
{ "token", new Token()
{
Confirmation = JsonSerializer.Serialize(cnf)
}
}
}
};
var thumbprint = refreshToken.GetProofKeyThumbprints().Single().Thumbprint;
Assert.Equal(expected, thumbprint);
}
}

0 comments on commit 1e7db6b

Please sign in to comment.