Skip to content

Commit

Permalink
feat: add token exchange flow
Browse files Browse the repository at this point in the history
  • Loading branch information
Jeff-Tian committed Apr 24, 2023
1 parent c6f2838 commit aef89c5
Show file tree
Hide file tree
Showing 5 changed files with 240 additions and 33 deletions.
12 changes: 12 additions & 0 deletions hosts/Configuration/ClientsWeb.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Collections.Generic;
using Duende.IdentityServer;
using Duende.IdentityServer.Models;
using IdentityModel;

namespace IdentityServerHost.Configuration;

Expand Down Expand Up @@ -185,6 +186,17 @@ public static IEnumerable<Client> Get()

AllowedScopes = allowedScopes
},

new Client()
{
ClientId = "token-exchange-client",
ClientSecrets =
{
new Secret("token-exchange-client".Sha256())
},
AllowedGrantTypes = { OidcConstants.GrantTypes.TokenExchange },
AllowedScopes = allowedScopes
}
};
}
}
68 changes: 35 additions & 33 deletions hosts/main/IdentityServerExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using IdentityServerHost.Configuration;
using IdentityServerHost.Extensions;
using IdentityServerHost.Stores;
using IdentityServerHost.Validators;
using Microsoft.AspNetCore.Builder;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
Expand All @@ -16,41 +17,42 @@ internal static class IdentityServerExtensions
internal static WebApplicationBuilder ConfigureIdentityServer(this WebApplicationBuilder builder, string assembly)
{
var identityServer = builder.Services.AddIdentityServer(options =>
{
options.Events.RaiseSuccessEvents = true;
options.Events.RaiseFailureEvents = true;
options.Events.RaiseErrorEvents = true;
options.Events.RaiseInformationEvents = true;
{
options.Events.RaiseSuccessEvents = true;
options.Events.RaiseFailureEvents = true;
options.Events.RaiseErrorEvents = true;
options.Events.RaiseInformationEvents = true;
options.EmitScopesAsSpaceDelimitedStringInJwt = true;
options.Endpoints.EnableJwtRequestUri = true;
options.EmitScopesAsSpaceDelimitedStringInJwt = true;
options.Endpoints.EnableJwtRequestUri = true;
options.ServerSideSessions.UserDisplayNameClaimType = JwtClaimTypes.Name;
})
.AddOperationalStore(options =>
{
options.ConfigureDbContext = b =>
b.UseNpgsql(DatabaseHostingExtensions.GetConnectionString(builder),
sql => sql.MigrationsAssembly(assembly));
options.EnableTokenCleanup = true;
options.TokenCleanupInterval = 3600;
})
.AddServerSideSessions()
.AddInMemoryClients(Clients.Get())
.AddInMemoryIdentityResources(Resources.IdentityResources)
.AddInMemoryApiScopes(Resources.ApiScopes)
.AddInMemoryApiResources(Resources.ApiResources)
// .AddStaticSigningCredential()
.AddExtensionGrantValidator<Extensions.ExtensionGrantValidator>()
.AddExtensionGrantValidator<Extensions.NoSubjectExtensionGrantValidator>()
.AddJwtBearerClientAuthentication()
.AddAppAuthRedirectUriValidator()
.AddTestUsers(TestUsers.Users)
// .AddProfileService<HostProfileService>()
.AddCustomTokenRequestValidator<ParameterizedScopeTokenRequestValidator>()
.AddScopeParser<ParameterizedScopeParser>()
.AddMutualTlsSecretValidators()
options.ServerSideSessions.UserDisplayNameClaimType = JwtClaimTypes.Name;
})
.AddOperationalStore(options =>
{
options.ConfigureDbContext = b =>
b.UseNpgsql(DatabaseHostingExtensions.GetConnectionString(builder),
sql => sql.MigrationsAssembly(assembly));
options.EnableTokenCleanup = true;
options.TokenCleanupInterval = 3600;
})
.AddServerSideSessions()
.AddInMemoryClients(Clients.Get())
.AddInMemoryIdentityResources(Resources.IdentityResources)
.AddInMemoryApiScopes(Resources.ApiScopes)
.AddInMemoryApiResources(Resources.ApiResources)
// .AddStaticSigningCredential()
.AddExtensionGrantValidator<Extensions.ExtensionGrantValidator>()
.AddExtensionGrantValidator<Extensions.NoSubjectExtensionGrantValidator>()
.AddJwtBearerClientAuthentication()
.AddAppAuthRedirectUriValidator()
.AddTestUsers(TestUsers.Users)
// .AddProfileService<HostProfileService>()
.AddCustomTokenRequestValidator<ParameterizedScopeTokenRequestValidator>()
.AddScopeParser<ParameterizedScopeParser>()
.AddMutualTlsSecretValidators()
.AddExtensionGrantValidator<TokenExchangeGrantValidator>()
// .AddProfileService<CustomProfileService>()
;
//.AddInMemoryOidcProviders(new[]
Expand Down
71 changes: 71 additions & 0 deletions hosts/main/Validators/TokenExchangeGrantValidator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using Duende.IdentityServer.Models;
using Duende.IdentityServer.Validation;
using IdentityModel;

namespace IdentityServerHost.Validators;

public class TokenExchangeGrantValidator : IExtensionGrantValidator
{
private readonly ITokenValidator _validator;

public TokenExchangeGrantValidator(ITokenValidator validator)
{
_validator = validator;
}

public string GrantType => OidcConstants.GrantTypes.TokenExchange;

public async Task ValidateAsync(ExtensionGrantValidationContext context)
{
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidRequest);

var subjectToken = context.Request.Raw.Get(OidcConstants.TokenRequest.SubjectToken);

var subjectTokenType = context.Request.Raw.Get(OidcConstants.TokenRequest.SubjectTokenType);

if (string.IsNullOrWhiteSpace(subjectToken))
{
await Console.Error.WriteLineAsync("subject_token is missing");
return;
}

if (!string.Equals(subjectTokenType, OidcConstants.TokenTypeIdentifiers.AccessToken,
StringComparison.OrdinalIgnoreCase))
{
await Console.Error.WriteLineAsync("subject_token_type is not access_token");
return;
}

var validationResult = await _validator.ValidateIdentityTokenAsync(subjectToken);

if (validationResult.IsError)
{
await Console.Error.WriteLineAsync($"subject_token is invalid: {subjectToken}");
await Console.Error.WriteLineAsync(validationResult.Error);
return;
}

await Console.Error.WriteLineAsync(JsonSerializer.Serialize(validationResult.Claims, new JsonSerializerOptions()
{
ReferenceHandler = ReferenceHandler.Preserve
}));

var sub = validationResult.Claims.FirstOrDefault(c => c.Type == JwtClaimTypes.Subject)?.Value ?? "unknown-sub";
var clientId = validationResult.Claims.FirstOrDefault(c => c.Type == JwtClaimTypes.ClientId)?.Value ??
validationResult.Claims.FirstOrDefault(c => c.Type == JwtClaimTypes.Audience)?.Value ??
"unknown-client";

context.Request.ClientId = clientId;
context.Result = new GrantValidationResult(sub, GrantType, validationResult.Claims, clientId,
new Dictionary<string, object>
{
{ OidcConstants.TokenResponse.IssuedTokenType, OidcConstants.TokenTypeIdentifiers.AccessToken },
});
}
}
94 changes: 94 additions & 0 deletions hosts/main/wwwroot/js/token-exchange-flow.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
$(document).ready(function () {

const selfHost = location.protocol + "//" + location.hostname + (location.port ? ":" + location.port : "");

const locale = getParameterByName("lang");
if (!locale)
window.location.href = window.location.href + "?lang=en-US";

const clientId = "token-exchange-client";

const messageLog = document.getElementById("ResponseOutput");

function logMessage(message, isError) {
var node = document.createElement("P");
if (isError && isError === true) {
node.setAttribute("style", "background-color: lightcoral; font-weight: bold");
}
var messageElement = document.createTextNode(message);
node.appendChild(messageElement);
messageLog.appendChild(node);
}
$("#gobutton").click(function (e) {
e.preventDefault();
messageLog.innerHTML = ""; //Clear messages output
logMessage("Starting token exchange flow with client: '" + clientId + "'");
$.ajax({
url: selfHost + "/api/v1/xsrf",
type: "GET",
xhrFields: {
withCredentials: true
},
success: function () {
console.log("Xsrf endpoint returned. cookies=" + document.cookie);
const token = getCookie("XSRF-TOKEN");
const subjectToken = $('#subject-token').val();

$.ajax({
type: "POST",
xhrFields: {
withCredentials: true
},
crossDomain: true,
beforeSend: function (xhrObj) {
xhrObj.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
xhrObj.setRequestHeader("X-XSRF-TOKEN", token);
},
url: selfHost + "/connect/token",
data: encodeURIComponent("client_id") + "=" + encodeURIComponent(clientId) + "&"
+ encodeURIComponent("client_secret") + "=" + encodeURIComponent(clientId) + "&"
+ encodeURIComponent("scope") + "=" + encodeURIComponent("openid profile") + "&" + encodeURIComponent("grant_type") + "=" + encodeURIComponent("urn:ietf:params:oauth:grant-type:token-exchange") + "&" + encodeURIComponent("subject_token") + "=" + encodeURIComponent(subjectToken) + "&" + encodeURIComponent("subject_token_type") + "=" + encodeURIComponent("urn:ietf:params:oauth:token-type:access_token")
,
success: function () {
},
complete: function (jqXHR) {
console.log(jqXHR.status, jqXHR.responseText);
if (jqXHR.status === 200) {
logMessage("太好了 —— 得到了响应! All good - we got a response!");
logMessage(jqXHR.responseText);
} else {
logMessage(jqXHR.responseText, true);
}
}
});
}
});
});
});


function getCookie(cname) {
var name = cname + "=";
var decodedCookie = decodeURIComponent(document.cookie);
var ca = decodedCookie.split(";");
for (var i = 0; i < ca.length; i++) {
var c = ca[i];
while (c.charAt(0) === " ") {
c = c.substring(1);
}
if (c.indexOf(name) === 0) {
return c.substring(name.length, c.length);
}
}
return "";
}

function getParameterByName(name, url) {
if (!url) url = window.location.href;
name = name.replace(/[\[\]]/g, "\\$&");
var regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)"),
results = regex.exec(url);
if (!results) return null;
if (!results[2]) return "";
return decodeURIComponent(results[2].replace(/\+/g, " "));
}
28 changes: 28 additions & 0 deletions hosts/main/wwwroot/token-exchange-flow.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login with token exchange flow</title>
</head>
<body>
<a href="/">Home</a> > <span class="headline">Login with token exchange flow</span>
<h2>Log in</h2>
<ul>
</ul>
<form id="form1" method="post" action="">
<label for="subject-token">Subject Token: </label><br/>
<textarea id="subject-token" style="width: 800px; height: 250px;"></textarea><br/>
<button id="gobutton">Sign me in!</button>
</form>
<h3>Output</h3>
<div id="ResponseOutput">

</div>
<script src="js/polyfills.js" nonce="{{nonce}}"></script>
<script src="js/oidc-client.slim.min.js" nonce="{{nonce}}"></script>
<script src="lib/jquery/dist/jquery.js"></script>
<script src="js/qrcode.min.js" nonce="{{nonce}}"></script>
<script src="js/token-exchange-flow.js?nonce={{nonce}}" nonce="{{nonce}}"></script>
</body>
</html>

0 comments on commit aef89c5

Please sign in to comment.