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
19 changes: 5 additions & 14 deletions .github/workflows/dotnet.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,11 @@ jobs:
runs-on: ${{ matrix.os }}

steps:
- uses: actions/checkout@v2
- name: Setup .NET Core 3.1
uses: actions/setup-dotnet@v1
- uses: actions/checkout@v4
- name: Setup .NET 10
uses: actions/setup-dotnet@v4
with:
dotnet-version: '3.1.x'
- name: Setup .NET 5.0
uses: actions/setup-dotnet@v1
with:
dotnet-version: '5.0.x'
- name: Fix Windows VM bug
shell: bash
if: matrix.os == 'windows-latest'
run: |
dotnet nuget locals all --clear
dotnet-version: '10.0.x'
- name: Restore
run: dotnet restore
- name: Build
Expand All @@ -41,7 +32,7 @@ jobs:
run: dotnet pack -c Release
- name: upload artifact
if: matrix.os == 'ubuntu-latest'
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v4
with:
name: nupkg
path: '**/*.nupkg'
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# OAuth Token Exchange (RFC 8693) for IdentityServer

An implementation of OAuth token exchange ([RFC 8693](https://www.rfc-editor.org/rfc/rfc8693.html)) for IdentityServer4 and Duende IdentityServer. This implementation provides the required abstractions for token exchange with extensibility points to implement your own authorization rules, with default implementation covering an API to API scenario.
An implementation of OAuth token exchange ([RFC 8693](https://www.rfc-editor.org/rfc/rfc8693.html)) for [Open.IdentityServer](https://github.com/RockSolidKnowledge/Open.IdentityServer) and Duende IdentityServer. This implementation provides the required abstractions for token exchange with extensibility points to implement your own authorization rules, with default implementation covering an API to API scenario.

This library includes:

Expand All @@ -13,10 +13,11 @@ To learn more about token exchange and why it is useful for microservices and AP

## Installation

This library is available as a nuget package for both IdentityServer4 and Duende IdentityServer.
This library is available as a nuget package for both Open.IdentityServer and Duende IdentityServer.

- [Rsk.TokenExchange.Open.IdentityServer](https://www.nuget.org/packages/Rsk.TokenExchange.Open.IdentityServer/)
- [Rsk.TokenExchange.DuendeIdentityServer](https://www.nuget.org/packages/Rsk.TokenExchange.DuendeIdentityServer/)
- [Rsk.TokenExchange.IdentityServer4](https://www.nuget.org/packages/Rsk.TokenExchange.IdentityServer4/)


Once you have installed the correct nuget package, you can enable the token exchange grant type by using the `AddTokenExchange` extension on `IdentityServerBuilder`.

Expand Down
4 changes: 2 additions & 2 deletions Rsk.TokenExchange.sln
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Rsk.TokenExchange.Tests", "
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Rsk.TokenExchange.Tests.Integration", "test\Rsk.TokenExchange.Tests.Integration\Rsk.TokenExchange.Tests.Integration.csproj", "{98215ED6-5DE9-42AD-BF08-176833F1F22C}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Rsk.TokenExchange.IdentityServer4", "src\Rsk.TokenExchange.IdentityServer4\Rsk.TokenExchange.IdentityServer4.csproj", "{D41B4200-C0A6-488A-9FC5-C0AD075A8228}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Rsk.TokenExchange.Open.IdentityServer", "src\Rsk.TokenExchange.Open.IdentityServer\Rsk.TokenExchange.Open.IdentityServer.csproj", "{D41B4200-C0A6-488A-9FC5-C0AD075A8228}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Rsk.TokenExchange.IdentityServer4.Tests", "test\Rsk.TokenExchange.IdentityServer4.Tests\Rsk.TokenExchange.IdentityServer4.Tests.csproj", "{3B3A599B-0B55-4146-A96D-A6A4E1DC1E77}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Rsk.TokenExchange.Open.IdentityServer.Tests", "test\Rsk.TokenExchange.Open.IdentityServer.Tests\Rsk.TokenExchange.Open.IdentityServer.Tests.csproj", "{3B3A599B-0B55-4146-A96D-A6A4E1DC1E77}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Rsk.TokenExchange.DuendeIdentityServer", "src\Rsk.TokenExchange.DuendeIdentityServer\Rsk.TokenExchange.DuendeIdentityServer.csproj", "{4F826FC4-8C6B-43A0-9EB3-031400C01427}"
EndProject
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Threading;
using System.Threading.Tasks;
using Duende.IdentityServer.Validation;
using Rsk.TokenExchange.Validators.Adaptors;
Expand All @@ -24,7 +25,7 @@ public IdentityServerSubjectTokenValidator(ITokenValidator tokenValidator)
/// <inheritdoc />
public async Task<TokenValidationResult> ValidateAccessToken(string token)
{
var result = await tokenValidator.ValidateAccessTokenAsync(token);
var result = await tokenValidator.ValidateAccessTokenAsync(token, expectedScope: null, CancellationToken.None);
return new TokenValidationResult(result.IsError, result.Claims);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>netcoreapp3.1;net5.0</TargetFrameworks>
<TargetFramework>net10.0</TargetFramework>
<Authors>Rock Solid Knowledge Ltd</Authors>
<Description>Token Exchange (RFC 8693) implementation for Deunde IdentityServer.</Description>
<PackageProjectUrl>https://github.com/RockSolidKnowledge/TokenExchange</PackageProjectUrl>
Expand All @@ -15,7 +15,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Duende.IdentityServer" Version="5.2.1" />
<PackageReference Include="Duende.IdentityServer" Version="8.0.10" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading;
using System.Threading.Tasks;
using Duende.IdentityModel;
using Duende.IdentityServer.Models;
using Duende.IdentityServer.Validation;
using IdentityModel;
using Microsoft.Extensions.Logging;
using Rsk.TokenExchange.Exceptions;
using Rsk.TokenExchange.Validators;
Expand Down Expand Up @@ -46,10 +47,15 @@ public TokenExchangeExtensionGrantValidator(
public string GrantType => TokenExchangeConstants.GrantType;

/// <inheritdoc />
public async Task ValidateAsync(ExtensionGrantValidationContext context)
public async Task ValidateAsync(ExtensionGrantValidationContext context, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(context);

// warn on no client auth
if (context.Request.Secret == null) logger.LogInformation("Recieved unauthenticated token exchange request");
if (context.Request.Secret == null)
{
logger.LogInformation("Received unauthenticated token exchange request");
}

// parse request
ITokenExchangeRequest tokenExchangeRequest;
Expand Down Expand Up @@ -105,15 +111,14 @@ public async Task ValidateAsync(ExtensionGrantValidationContext context)
/// </summary>
public virtual void UpdateRequest(ValidatedTokenRequest request, IEnumerable<Claim> claims)
{
if (request == null) throw new ArgumentNullException(nameof(request));
if (claims == null) throw new ArgumentNullException(nameof(claims));
ArgumentNullException.ThrowIfNull(request);
ArgumentNullException.ThrowIfNull(claims);

// update client_id to act as original client app
var clientId = claims.First(x => x.Type == JwtClaimTypes.ClientId).Value;
request.ClientId = clientId;
}

private static readonly Dictionary<string, object> CustomResponseParameters = new Dictionary<string, object>
{{TokenExchangeConstants.ResponseParameters.IssuedTokenType, TokenExchangeConstants.TokenTypes.AccessToken}};
private static readonly Dictionary<string, object> CustomResponseParameters = new() {{TokenExchangeConstants.ResponseParameters.IssuedTokenType, TokenExchangeConstants.TokenTypes.AccessToken}};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@
using Rsk.TokenExchange.Configuration;
using Rsk.TokenExchange.Validators.Adaptors;

namespace Rsk.TokenExchange.IdentityServer4
namespace Rsk.TokenExchange.Open.IdentityServer
{
/// <summary>
/// Token exchange registrations for IIdentityServerBuilder
/// </summary>
public static class IdentityServerBuilderExtensions
{
/// <summary>
/// Adds the default token exchange registrations for IdentityServer4.
/// Adds the default token exchange registrations for Open.IdentityServer.
/// Uses the default authorization policy, designed for token exchange between microservices.
/// </summary>
public static IIdentityServerBuilder AddTokenExchange(this IIdentityServerBuilder builder)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
using System.Threading.Tasks;
using IdentityServer4.Validation;
using Open.IdentityServer.Validation;
using Rsk.TokenExchange.Validators.Adaptors;
using TokenValidationResult = Rsk.TokenExchange.Validators.Adaptors.TokenValidationResult;

namespace Rsk.TokenExchange.IdentityServer4
namespace Rsk.TokenExchange.Open.IdentityServer
{
/// <summary>
/// Wrapper around IdentityServer4's ITokenValidator
/// Wrapper around Open.IdentityServer's ITokenValidator
/// </summary>
public class IdentityServerSubjectTokenValidator : ITokenValidatorAdaptor
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<Authors>Rock Solid Knowledge Ltd</Authors>
<Description>Token Exchange (RFC 8693) implementation for IdentityServer4.</Description>
<Description>Token Exchange (RFC 8693) implementation for Open.IdentityServer.</Description>
<PackageProjectUrl>https://github.com/RockSolidKnowledge/TokenExchange</PackageProjectUrl>
<PackageReleaseNotes>https://github.com/RockSolidKnowledge/TokenExchange/releases</PackageReleaseNotes>
<Copyright>Copyright 2021 (c) Rock Solid Knowledge Ltd. All rights reserved.</Copyright>
<PackageTags>IdentityServer OAuth Token Exchange IdentityServer4</PackageTags>
<PackageTags>IdentityServer OAuth Token Exchange IdentityServer4 Open.IdentityServer</PackageTags>
<IncludeSymbols>true</IncludeSymbols>
<PackageIcon>icon.png</PackageIcon>
<PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<RootNamespace>Rsk.TokenExchange.Open.IdentityServer</RootNamespace>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="IdentityServer4" Version="4.1.2" />
<PackageReference Include="Open.IdentityServer" Version="3.0.0" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using IdentityModel;
using IdentityServer4.Models;
using IdentityServer4.Validation;
using Open.IdentityServer.Models;
using Open.IdentityServer.Validation;
using Microsoft.Extensions.Logging;
using Open.IdentityServer;
using Rsk.TokenExchange.Exceptions;
using Rsk.TokenExchange.Validators;

namespace Rsk.TokenExchange.IdentityServer4
namespace Rsk.TokenExchange.Open.IdentityServer
{
/// <summary>
/// Extension grant validator for OAuth Token Exchange (RFC 8693).
Expand Down Expand Up @@ -48,8 +48,16 @@ public TokenExchangeExtensionGrantValidator(
/// <inheritdoc />
public async Task ValidateAsync(ExtensionGrantValidationContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}

// warn on no client auth
if (context.Request.Secret == null) logger.LogInformation("Recieved unauthenticated token exchange request");
if (context.Request.Secret == null)
{
logger.LogInformation("Received unauthenticated token exchange request");
}

// parse request
ITokenExchangeRequest tokenExchangeRequest;
Expand Down Expand Up @@ -105,15 +113,14 @@ public async Task ValidateAsync(ExtensionGrantValidationContext context)
/// </summary>
public virtual void UpdateRequest(ValidatedTokenRequest request, IEnumerable<Claim> claims)
{
if (request == null) throw new ArgumentNullException(nameof(request));
if (claims == null) throw new ArgumentNullException(nameof(claims));
ArgumentNullException.ThrowIfNull(request);
ArgumentNullException.ThrowIfNull(claims);

// update client_id to act as original client app
var clientId = claims.First(x => x.Type == JwtClaimTypes.ClientId).Value;
request.ClientId = clientId;
}

private static readonly Dictionary<string, object> CustomResponseParameters = new Dictionary<string, object>
{{TokenExchangeConstants.ResponseParameters.IssuedTokenType, TokenExchangeConstants.TokenTypes.AccessToken}};
private static readonly Dictionary<string, object> CustomResponseParameters = new() {{TokenExchangeConstants.ResponseParameters.IssuedTokenType, TokenExchangeConstants.TokenTypes.AccessToken}};
}
}
3 changes: 0 additions & 3 deletions src/Rsk.TokenExchange/Exceptions/ClaimsParsingException.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,5 @@ public SubjectParsingException(string message) : base(message) { }

/// <inheritdoc />
public SubjectParsingException(string message, Exception inner) : base(message, inner) { }

/// <inheritdoc />
protected SubjectParsingException(SerializationInfo info, StreamingContext context) : base(info, context) { }
}
}
3 changes: 0 additions & 3 deletions src/Rsk.TokenExchange/Exceptions/InvalidRequestException.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,5 @@ public InvalidRequestException(string message) : base(message) { }

/// <inheritdoc />
public InvalidRequestException(string message, Exception inner) : base(message, inner) { }

/// <inheritdoc />
protected InvalidRequestException(SerializationInfo info, StreamingContext context) : base(info, context) { }
}
}
3 changes: 0 additions & 3 deletions src/Rsk.TokenExchange/Exceptions/TokenExchangeException.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,5 @@ public TokenExchangeException(string message) : base(message) { }

/// <inheritdoc />
public TokenExchangeException(string message, Exception inner) : base(message, inner) { }

/// <inheritdoc />
protected TokenExchangeException(SerializationInfo info, StreamingContext context) : base(info, context) { }
}
}
8 changes: 4 additions & 4 deletions src/Rsk.TokenExchange/Models/Actor.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Newtonsoft.Json;
using System.Text.Json.Serialization;

namespace Rsk.TokenExchange
{
Expand All @@ -12,21 +12,21 @@ public class Actor
/// The original issuer (iss) of exchanged token.
/// Useful for cross-tenant/identity provider token exchange.
/// </summary>
[JsonProperty("iss")]
[JsonPropertyName("iss")]
public string Issuer { get; set; }

/// <summary>
/// The original client ID (client_id) of the exchanged token.
/// Useful for tracking who originally requested the exchanged token.
/// </summary>
[JsonProperty("client_id")]
[JsonPropertyName("client_id")]
public string ClientId { get; set; }

/// <summary>
/// The inner actor claim.
/// Useful for multiple token exchange requests.
/// </summary>
[JsonProperty("act")]
[JsonPropertyName("act")]
public string InnerActor { get; set; }

}
Expand Down
10 changes: 7 additions & 3 deletions src/Rsk.TokenExchange/Models/TokenExchangeRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,13 @@ public class TokenExchangeRequest : ITokenExchangeRequest
/// <exception cref="InvalidRequestException">Throw on missing grant_type, subject_token, and subject_token_type.</exception>
public TokenExchangeRequest(string clientId, NameValueCollection request)
{
if (string.IsNullOrWhiteSpace(clientId)) throw new ArgumentNullException(nameof(clientId));
if (request == null) throw new ArgumentNullException(nameof(request));

if (string.IsNullOrWhiteSpace(clientId))
{
throw new ArgumentNullException(nameof(clientId));
}

ArgumentNullException.ThrowIfNull(request);

ClientId = clientId;

// grant type must be urn:ietf:params:oauth:grant-type:token-exchange
Expand Down
16 changes: 10 additions & 6 deletions src/Rsk.TokenExchange/Parsers/TokenExchangeClaimsParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using Microsoft.IdentityModel.JsonWebTokens;
using Newtonsoft.Json;
using Rsk.TokenExchange.Exceptions;

namespace Rsk.TokenExchange
Expand All @@ -17,10 +18,13 @@ public class TokenExchangeClaimsParser : ITokenExchangeClaimsParser
/// <exception cref="SubjectParsingException">Throws when more than one subject claim is present in the subject token claims.</exception>
public virtual Task<string> ParseSubject(IEnumerable<Claim> subjectClaims, ITokenExchangeRequest request)
{
if (subjectClaims == null) throw new ArgumentNullException(nameof(subjectClaims));
ArgumentNullException.ThrowIfNull(subjectClaims);

var subjectValues = subjectClaims.Where(x => x.Type == "sub").Select(x => x.Value).ToList();
if (1 < subjectValues.Count) throw new SubjectParsingException("More than one subject claim present on token exchange request");
if (subjectValues.Count > 1)
{
throw new SubjectParsingException("More than one subject claim present on token exchange request");
}

return Task.FromResult(subjectValues.FirstOrDefault());
}
Expand All @@ -29,8 +33,8 @@ public virtual Task<string> ParseSubject(IEnumerable<Claim> subjectClaims, IToke
/// <returns>Returns the subject claims along with an actor claim when client_id != subject token client_id</returns>
public virtual Task<IEnumerable<Claim>> ParseClaims(IEnumerable<Claim> subjectClaims, ITokenExchangeRequest request)
{
if (subjectClaims == null) throw new ArgumentNullException(nameof(subjectClaims));
if (request == null) throw new ArgumentNullException(nameof(request));
ArgumentNullException.ThrowIfNull(subjectClaims);
ArgumentNullException.ThrowIfNull(request);

var claims = subjectClaims.ToList();

Expand All @@ -43,7 +47,7 @@ public virtual Task<IEnumerable<Claim>> ParseClaims(IEnumerable<Claim> subjectCl
var actor = new Actor {ClientId = request.ClientId, InnerActor = previousActor?.Value};
claims.Add(new Claim(
"act",
JsonConvert.SerializeObject(actor, new JsonSerializerSettings {NullValueHandling = NullValueHandling.Ignore}),
JsonSerializer.Serialize(actor, new JsonSerializerOptions {DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull}),
JsonClaimValueTypes.Json));
}

Expand Down
Loading
Loading