From 95bbc07526159a20dea319d4e9b486654d3213b4 Mon Sep 17 00:00:00 2001 From: "joel.thoms" Date: Wed, 17 Aug 2016 18:17:44 -0700 Subject: [PATCH 1/2] Added support for realm switching in a multi-tenant environment. --- src/KeycloakIdentityModel/KeycloakIdentity.cs | 81 +++++++++---------- .../KeycloakIdentityModel.csproj | 12 +++ .../DefaultKeycloakParameters.cs | 9 +++ .../Models/Configuration/IKeycloakSettings.cs | 2 + .../Messages/RefreshAccessTokenMessage.cs | 7 +- .../Messages/RequestAccessTokenMessage.cs | 7 +- .../Utilities/KeycloakTokenHandler.cs | 9 ++- .../Utilities/OidcDataManager.cs | 57 ++++++++----- src/KeycloakIdentityModel/packages.config | 3 + .../KeycloakAuthenticationOptions.cs | 9 +++ .../Extensions/AppBuilderExtension.cs | 4 +- .../KeycloakAuthenticationHandler.cs | 20 ++--- .../KeycloakAuthenticationMiddleware.cs | 2 +- 13 files changed, 135 insertions(+), 87 deletions(-) diff --git a/src/KeycloakIdentityModel/KeycloakIdentity.cs b/src/KeycloakIdentityModel/KeycloakIdentity.cs index 1fd9e67..acd8f7d 100644 --- a/src/KeycloakIdentityModel/KeycloakIdentity.cs +++ b/src/KeycloakIdentityModel/KeycloakIdentity.cs @@ -14,6 +14,7 @@ using KeycloakIdentityModel.Models.Responses; using KeycloakIdentityModel.Utilities; using KeycloakIdentityModel.Utilities.ClaimMapping; +using Microsoft.Owin; using Newtonsoft.Json.Linq; namespace KeycloakIdentityModel @@ -124,34 +125,35 @@ public override bool TryRemoveClaim(Claim claim) /// public override ClaimsIdentity Clone() { - return Task.Run(ToClaimsIdentityAsync).Result; + throw new NotImplementedException(); + //return Task.Run(() => ToClaimsIdentityAsync(context)).Result; } /// /// Refreshes and re-authenticates the current identity from the Keycloak instance (only if necessary) /// /// - public Task RefreshIdentityAsync() + public Task RefreshIdentityAsync(IOwinContext context) { - return GetClaimsAsync(); + return GetClaimsAsync(context); } /// /// Refreshes and returns the updated claims for the identity (refreshes only if necessary) /// /// - public Task> GetUpdatedClaimsAsync() + public Task> GetUpdatedClaimsAsync(IOwinContext context) { - return GetClaimsAsync(); + return GetClaimsAsync(context); } /// /// Returns a static base representation of the identity as a claims identity /// /// - public async Task ToClaimsIdentityAsync() + public async Task ToClaimsIdentityAsync(IOwinContext context) { - return new ClaimsIdentity(await GetClaimsAsync(), AuthenticationType); + return new ClaimsIdentity(await GetClaimsAsync(context), AuthenticationType); } #endregion @@ -164,12 +166,12 @@ public async Task ToClaimsIdentityAsync() /// /// /// - public static Task ConvertFromClaimsIdentityAsync(IKeycloakParameters parameters, + public static Task ConvertFromClaimsIdentityAsync(IOwinContext context, IKeycloakParameters parameters, ClaimsIdentity identity) { if (parameters == null) throw new ArgumentNullException(nameof(parameters)); if (identity == null) throw new ArgumentNullException(nameof(identity)); - return ConvertFromClaimsAsync(parameters, identity.Claims); + return ConvertFromClaimsAsync(context, parameters, identity.Claims); } /// @@ -178,7 +180,7 @@ public static Task ConvertFromClaimsIdentityAsync(IKeycloakPar /// /// /// - public static Task ConvertFromClaimsAsync(IKeycloakParameters parameters, + public static Task ConvertFromClaimsAsync(IOwinContext context, IKeycloakParameters parameters, IEnumerable claims) { if (parameters == null) throw new ArgumentNullException(nameof(parameters)); @@ -189,7 +191,7 @@ public static Task ConvertFromClaimsAsync(IKeycloakParameters var accessToken = claimLookup[Constants.ClaimTypes.AccessToken].FirstOrDefault(); var refreshToken = claimLookup[Constants.ClaimTypes.RefreshToken].FirstOrDefault(); - return ConvertFromJwtAsync(parameters, accessToken, refreshToken, idToken); + return ConvertFromJwtAsync(context, parameters, accessToken, refreshToken, idToken); } /// @@ -198,12 +200,12 @@ public static Task ConvertFromClaimsAsync(IKeycloakParameters /// /// /// - public static Task ConvertFromTokenResponseAsync(IKeycloakParameters parameters, + public static Task ConvertFromTokenResponseAsync(IOwinContext context, IKeycloakParameters parameters, TokenResponse message) { if (parameters == null) throw new ArgumentNullException(nameof(parameters)); if (message == null) throw new ArgumentNullException(nameof(message)); - return ConvertFromJwtAsync(parameters, message.AccessToken, message.RefreshToken, message.IdToken); + return ConvertFromJwtAsync(context, parameters, message.AccessToken, message.RefreshToken, message.IdToken); } /// @@ -213,7 +215,7 @@ public static Task ConvertFromTokenResponseAsync(IKeycloakPara /// /// /// - public static async Task ConvertFromAuthResponseAsync(IKeycloakParameters parameters, + public static async Task ConvertFromAuthResponseAsync(IOwinContext context, IKeycloakParameters parameters, AuthorizationResponse response, Uri baseUri) { if (parameters == null) throw new ArgumentNullException(nameof(parameters)); @@ -221,8 +223,8 @@ public static async Task ConvertFromAuthResponseAsync(IKeycloa if (baseUri == null) throw new ArgumentNullException(nameof(baseUri)); response.ThrowIfError(); - var message = new RequestAccessTokenMessage(baseUri, parameters, response); - return await ConvertFromTokenResponseAsync(parameters, await message.ExecuteAsync()); + var message = new RequestAccessTokenMessage(context, baseUri, parameters, response); + return await ConvertFromTokenResponseAsync(context, parameters, await message.ExecuteAsync()); } /// @@ -233,7 +235,7 @@ public static async Task ConvertFromAuthResponseAsync(IKeycloa /// /// /// - public static async Task ConvertFromJwtAsync(IKeycloakParameters parameters, + public static async Task ConvertFromJwtAsync(IOwinContext context, IKeycloakParameters parameters, string accessToken, string refreshToken = null, string idToken = null) { if (parameters == null) throw new ArgumentNullException(nameof(parameters)); @@ -242,12 +244,12 @@ public static async Task ConvertFromJwtAsync(IKeycloakParamete var kcIdentity = new KeycloakIdentity(parameters); try { - await kcIdentity.CopyFromJwt(accessToken, refreshToken, idToken); + await kcIdentity.CopyFromJwt(context, accessToken, refreshToken, idToken); } catch (SecurityTokenExpiredException) { // Load new identity from token endpoint via refresh token (if possible) - await kcIdentity.RefreshIdentity(refreshToken); + await kcIdentity.RefreshIdentity(context, refreshToken); } return kcIdentity; } @@ -259,14 +261,14 @@ public static async Task ConvertFromJwtAsync(IKeycloakParamete /// /// /// - public static async Task GenerateLoginUriAsync(IKeycloakParameters parameters, Uri baseUri, + public static async Task GenerateLoginUriAsync(IOwinContext context, IKeycloakParameters parameters, Uri baseUri, string state = null) { if (parameters == null) throw new ArgumentNullException(nameof(parameters)); if (baseUri == null) throw new ArgumentNullException(nameof(baseUri)); // Generate login URI and data - var uriManager = await OidcDataManager.GetCachedContextAsync(parameters); + var uriManager = await OidcDataManager.GetCachedContextAsync(context, parameters); var loginParams = uriManager.BuildAuthorizationEndpointContent(baseUri, state ?? Guid.NewGuid().ToString()); var loginUrl = uriManager.GetAuthorizationEndpoint(); @@ -281,12 +283,12 @@ public static async Task GenerateLoginUriAsync(IKeycloakParameters paramete /// /// /// - public static async Task GenerateLoginCallbackUriAsync(IKeycloakParameters parameters, Uri baseUri) + public static async Task GenerateLoginCallbackUriAsync(IOwinContext context, IKeycloakParameters parameters, Uri baseUri) { if (parameters == null) throw new ArgumentNullException(nameof(parameters)); if (baseUri == null) throw new ArgumentNullException(nameof(baseUri)); - return (await OidcDataManager.GetCachedContextAsync(parameters)).GetCallbackUri(baseUri); + return (await OidcDataManager.GetCachedContextAsync(context, parameters)).GetCallbackUri(baseUri); } /// @@ -296,14 +298,14 @@ public static async Task GenerateLoginCallbackUriAsync(IKeycloakParameters /// /// /// - public static async Task GenerateLogoutUriAsync(IKeycloakParameters parameters, Uri baseUri, + public static async Task GenerateLogoutUriAsync(IOwinContext context, IKeycloakParameters parameters, Uri baseUri, string redirectUrl = null) { if (parameters == null) throw new ArgumentNullException(nameof(parameters)); if (baseUri == null) throw new ArgumentNullException(nameof(baseUri)); // Generate logout URI and data - var uriManager = await OidcDataManager.GetCachedContextAsync(parameters); + var uriManager = await OidcDataManager.GetCachedContextAsync(context, parameters); var logoutParams = uriManager.BuildEndSessionEndpointContent(baseUri, null, redirectUrl); var logoutUrl = uriManager.GetEndSessionEndpoint(); @@ -317,7 +319,7 @@ public static async Task GenerateLogoutUriAsync(IKeycloakParameters paramet /// /// /// - public static bool TryValidateParameters(IKeycloakParameters parameters) + public static bool TryValidateParameters(IOwinContext context, IKeycloakParameters parameters) { try { @@ -342,7 +344,7 @@ public static void ValidateParameters(IKeycloakParameters parameters) // Verify required parameters if (parameters.KeycloakUrl == null) throw new ArgumentNullException(nameof(parameters.KeycloakUrl)); - if (parameters.Realm == null) + if (parameters.Realm == null && parameters.MultiTenantRealmSelector == null) throw new ArgumentNullException(nameof(parameters.Realm)); // Set default parameters @@ -362,17 +364,6 @@ public static void ValidateParameters(IKeycloakParameters parameters) if (parameters.PostLogoutRedirectUrl != null && !Uri.IsWellFormedUriString(parameters.PostLogoutRedirectUrl, UriKind.RelativeOrAbsolute)) throw new ArgumentException(nameof(parameters.PostLogoutRedirectUrl)); - - // Attempt to refresh OIDC metadata from endpoint (on separate thread) - try - { - Task.Run(() => OidcDataManager.GetCachedContextAsync(parameters)).Wait(); - } - catch (Exception exception) - { - throw new ArgumentException("Invalid Keycloak server parameters specified: See inner for server error", - exception); - } } #endregion @@ -430,7 +421,7 @@ private IEnumerable GetCurrentClaims() return _kcClaims.Concat(_userClaims); } - private async Task> GetClaimsAsync() + private async Task> GetClaimsAsync(IOwinContext context) { await _refreshLock.WaitAsync(); try @@ -443,7 +434,7 @@ private async Task> GetClaimsAsync() throw new Exception("Both the access token and the refresh token have expired"); // Load new identity from token endpoint via refresh token - await RefreshIdentity(_refreshToken.RawData); + await RefreshIdentity(context, _refreshToken.RawData); } return GetCurrentClaims(); @@ -454,13 +445,13 @@ private async Task> GetClaimsAsync() } } - protected async Task CopyFromJwt(string accessToken, string refreshToken = null, string idToken = null) + protected async Task CopyFromJwt(IOwinContext context, string accessToken, string refreshToken = null, string idToken = null) { if (accessToken == null) throw new ArgumentException(nameof(accessToken)); // Validate JWTs provided var tokenHandler = new KeycloakTokenHandler(); - var uriManager = await OidcDataManager.GetCachedContextAsync(_parameters); + var uriManager = await OidcDataManager.GetCachedContextAsync(context, _parameters); SecurityToken accessSecurityToken, idSecurityToken = null, refreshSecurityToken = null; @@ -491,11 +482,11 @@ protected async Task CopyFromJwt(string accessToken, string refreshToken = null, _refreshToken = refreshSecurityToken as JwtSecurityToken; } - protected async Task RefreshIdentity(string refreshToken) + protected async Task RefreshIdentity(IOwinContext context, string refreshToken) { var respMessage = - await new RefreshAccessTokenMessage(_parameters, refreshToken).ExecuteAsync(); - await CopyFromJwt(respMessage.AccessToken, respMessage.RefreshToken, respMessage.IdToken); + await new RefreshAccessTokenMessage(context, _parameters, refreshToken).ExecuteAsync(); + await CopyFromJwt(context, respMessage.AccessToken, respMessage.RefreshToken, respMessage.IdToken); IsTouched = true; } diff --git a/src/KeycloakIdentityModel/KeycloakIdentityModel.csproj b/src/KeycloakIdentityModel/KeycloakIdentityModel.csproj index 6e81d1f..64b27fb 100644 --- a/src/KeycloakIdentityModel/KeycloakIdentityModel.csproj +++ b/src/KeycloakIdentityModel/KeycloakIdentityModel.csproj @@ -44,10 +44,22 @@ ..\packages\Microsoft.IdentityModel.Protocol.Extensions.1.0.2.206221351\lib\net45\Microsoft.IdentityModel.Protocol.Extensions.dll True + + ..\packages\Microsoft.Owin.3.0.1\lib\net45\Microsoft.Owin.dll + True + + + ..\packages\Microsoft.Owin.Security.3.0.1\lib\net45\Microsoft.Owin.Security.dll + True + ..\packages\Newtonsoft.Json.7.0.1\lib\net45\Newtonsoft.Json.dll True + + ..\packages\Owin.1.0\lib\net40\Owin.dll + True + diff --git a/src/KeycloakIdentityModel/Models/Configuration/DefaultKeycloakParameters.cs b/src/KeycloakIdentityModel/Models/Configuration/DefaultKeycloakParameters.cs index 98bed68..d6c2b6c 100644 --- a/src/KeycloakIdentityModel/Models/Configuration/DefaultKeycloakParameters.cs +++ b/src/KeycloakIdentityModel/Models/Configuration/DefaultKeycloakParameters.cs @@ -1,4 +1,5 @@ using System; +using Microsoft.Owin; namespace KeycloakIdentityModel.Models.Configuration { @@ -23,6 +24,14 @@ public class DefaultKeycloakParameters : IKeycloakParameters /// public string Realm { get; set; } + /// + /// OPTIONAL: The MultiTenantRealmSelector is used to dynamically set the Realm for multi-tenant applications. + /// + /// + /// - The purpose is for multi-tenant platforms that require multiple Realms. + /// + public Func MultiTenantRealmSelector { get; set; } + /// /// The client ID to use for the application /// diff --git a/src/KeycloakIdentityModel/Models/Configuration/IKeycloakSettings.cs b/src/KeycloakIdentityModel/Models/Configuration/IKeycloakSettings.cs index f1fb71d..5a898e2 100644 --- a/src/KeycloakIdentityModel/Models/Configuration/IKeycloakSettings.cs +++ b/src/KeycloakIdentityModel/Models/Configuration/IKeycloakSettings.cs @@ -1,4 +1,5 @@ using System; +using Microsoft.Owin; namespace KeycloakIdentityModel.Models.Configuration { @@ -7,6 +8,7 @@ public interface IKeycloakParameters string AuthenticationType { get; } string KeycloakUrl { get; } string Realm { get; } + Func MultiTenantRealmSelector { get; } string ClientId { get; } string ClientSecret { get; } string Scope { get; } diff --git a/src/KeycloakIdentityModel/Models/Messages/RefreshAccessTokenMessage.cs b/src/KeycloakIdentityModel/Models/Messages/RefreshAccessTokenMessage.cs index 244e0af..d2198e5 100644 --- a/src/KeycloakIdentityModel/Models/Messages/RefreshAccessTokenMessage.cs +++ b/src/KeycloakIdentityModel/Models/Messages/RefreshAccessTokenMessage.cs @@ -3,18 +3,21 @@ using KeycloakIdentityModel.Models.Configuration; using KeycloakIdentityModel.Models.Responses; using KeycloakIdentityModel.Utilities; +using Microsoft.Owin; namespace KeycloakIdentityModel.Models.Messages { public class RefreshAccessTokenMessage : GenericMessage { - public RefreshAccessTokenMessage(IKeycloakParameters options, string refreshToken) + public RefreshAccessTokenMessage(IOwinContext context, IKeycloakParameters options, string refreshToken) : base(options) { if (refreshToken == null) throw new ArgumentNullException(); RefreshToken = refreshToken; + Context = context; } + private IOwinContext Context { get; } private string RefreshToken { get; } public override async Task ExecuteAsync() @@ -24,7 +27,7 @@ public override async Task ExecuteAsync() private async Task ExecuteHttpRequestAsync() { - var uriManager = await OidcDataManager.GetCachedContextAsync(Options); + var uriManager = await OidcDataManager.GetCachedContextAsync(Context, Options); var response = await SendHttpPostRequest(uriManager.GetTokenEndpoint(), diff --git a/src/KeycloakIdentityModel/Models/Messages/RequestAccessTokenMessage.cs b/src/KeycloakIdentityModel/Models/Messages/RequestAccessTokenMessage.cs index f5cef22..420f585 100644 --- a/src/KeycloakIdentityModel/Models/Messages/RequestAccessTokenMessage.cs +++ b/src/KeycloakIdentityModel/Models/Messages/RequestAccessTokenMessage.cs @@ -3,12 +3,13 @@ using KeycloakIdentityModel.Models.Configuration; using KeycloakIdentityModel.Models.Responses; using KeycloakIdentityModel.Utilities; +using Microsoft.Owin; namespace KeycloakIdentityModel.Models.Messages { public class RequestAccessTokenMessage : GenericMessage { - public RequestAccessTokenMessage(Uri baseUri, IKeycloakParameters options, + public RequestAccessTokenMessage(IOwinContext context, Uri baseUri, IKeycloakParameters options, AuthorizationResponse authResponse) : base(options) { @@ -17,10 +18,12 @@ public RequestAccessTokenMessage(Uri baseUri, IKeycloakParameters options, BaseUri = baseUri; AuthResponse = authResponse; + Context = context; } protected Uri BaseUri { get; } private AuthorizationResponse AuthResponse { get; } + private IOwinContext Context { get; } public override async Task ExecuteAsync() { @@ -29,7 +32,7 @@ public override async Task ExecuteAsync() private async Task ExecuteHttpRequestAsync() { - var uriManager = await OidcDataManager.GetCachedContextAsync(Options); + var uriManager = await OidcDataManager.GetCachedContextAsync(Context, Options); var response = await SendHttpPostRequest(uriManager.GetTokenEndpoint(), uriManager.BuildAccessTokenEndpointContent(BaseUri, AuthResponse.Code)); return await response.Content.ReadAsStringAsync(); diff --git a/src/KeycloakIdentityModel/Utilities/KeycloakTokenHandler.cs b/src/KeycloakIdentityModel/Utilities/KeycloakTokenHandler.cs index cd83ea2..069c2bd 100644 --- a/src/KeycloakIdentityModel/Utilities/KeycloakTokenHandler.cs +++ b/src/KeycloakIdentityModel/Utilities/KeycloakTokenHandler.cs @@ -6,14 +6,15 @@ using System.Threading.Tasks; using KeycloakIdentityModel.Models.Configuration; using Microsoft.IdentityModel; +using Microsoft.Owin; namespace KeycloakIdentityModel.Utilities { internal class KeycloakTokenHandler : JwtSecurityTokenHandler { - public static async Task ValidateTokenRemote(string jwt, IKeycloakParameters options) + public static async Task ValidateTokenRemote(IOwinContext context, string jwt, IKeycloakParameters options) { - var uriManager = await OidcDataManager.GetCachedContextAsync(options); + var uriManager = await OidcDataManager.GetCachedContextAsync(context, options); return await ValidateTokenRemote(jwt, uriManager); } @@ -48,9 +49,9 @@ public bool TryValidateToken(string jwt, IKeycloakParameters options, OidcDataMa } } - public async Task ValidateTokenAsync(string jwt, IKeycloakParameters options) + public async Task ValidateTokenAsync(IOwinContext context, string jwt, IKeycloakParameters options) { - var uriManager = await OidcDataManager.GetCachedContextAsync(options); + var uriManager = await OidcDataManager.GetCachedContextAsync(context, options); return ValidateToken(jwt, options, uriManager); } diff --git a/src/KeycloakIdentityModel/Utilities/OidcDataManager.cs b/src/KeycloakIdentityModel/Utilities/OidcDataManager.cs index 0d3bc3f..d7accf5 100644 --- a/src/KeycloakIdentityModel/Utilities/OidcDataManager.cs +++ b/src/KeycloakIdentityModel/Utilities/OidcDataManager.cs @@ -6,6 +6,7 @@ using KeycloakIdentityModel.Models.Configuration; using KeycloakIdentityModel.Utilities.Synchronization; using Microsoft.IdentityModel.Protocols; +using Microsoft.Owin; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -21,16 +22,20 @@ public class OidcDataManager private readonly IKeycloakParameters _options; private readonly ReaderWriterLockSlim _refreshLock = new ReaderWriterLockSlim(); + public static Func MultiTenantRealmSelector; + // Thread-safe pipeline locks private bool _cacheRefreshing; private DateTime _nextCachedRefreshTime; - protected OidcDataManager(IKeycloakParameters options) + protected OidcDataManager(IOwinContext context, IKeycloakParameters options) { _options = options; _nextCachedRefreshTime = DateTime.Now; - Authority = _options.KeycloakUrl + "/realms/" + _options.Realm; + var realm = MultiTenantRealmSelector != null ? MultiTenantRealmSelector(context) : options.Realm; + + Authority = _options.KeycloakUrl + "/realms/" + realm; MetadataEndpoint = new Uri(Authority + "/" + OpenIdProviderMetadataNames.Discovery); TokenValidationEndpoint = new Uri(Authority + "/tokens/validate"); } @@ -55,10 +60,10 @@ private class Metadata #region Context Caching - public static Task ValidateCachedContextAsync(IKeycloakParameters options) + public static Task ValidateCachedContextAsync(IOwinContext context, IKeycloakParameters options) { - var context = GetCachedContext(options.AuthenticationType); - return context.ValidateCachedContextAsync(); + return GetCachedContext(context, options.AuthenticationType) + .ValidateCachedContextAsync(); } private async Task ValidateCachedContextAsync() @@ -81,40 +86,47 @@ private async Task ValidateCachedContextAsync() } } - public static OidcDataManager GetCachedContext(IKeycloakParameters options) + public static OidcDataManager GetCachedContext(IOwinContext context, IKeycloakParameters options) { - return GetCachedContext(options.AuthenticationType); + return GetCachedContext(context, options.AuthenticationType); } - public static OidcDataManager GetCachedContext(string authType) + public static OidcDataManager GetCachedContext(IOwinContext context, string authType) { - var context = GetCachedContextSafe(authType); - if (context == null) + var oidc = GetCachedContextSafe(context, authType); + if (oidc == null) throw new Exception($"Could not find cached OIDC data manager for module '{authType}'"); - return context; + return oidc; } - public static Task GetCachedContextAsync(IKeycloakParameters options) + public static Task GetCachedContextAsync(IOwinContext context, IKeycloakParameters options) { - var context = GetCachedContextSafe(options.AuthenticationType); - return context != null ? Task.FromResult(context) : CreateCachedContext(options); + var oidc = GetCachedContextSafe(context, options.AuthenticationType); + return oidc != null ? Task.FromResult(oidc) : CreateCachedContext(context, options); } - private static OidcDataManager GetCachedContextSafe(string authType) + private static OidcDataManager GetCachedContextSafe(IOwinContext context, string authType) { OidcDataManager result; - return OidcManagerCache.TryGetValue(authType + CachedContextPostfix, out result) ? result : null; + var realmPrefix = GetRealmPrefix(context); + return OidcManagerCache.TryGetValue(realmPrefix + authType + CachedContextPostfix, out result) ? result : null; } - public static async Task CreateCachedContext(IKeycloakParameters options, + private static async Task CreateCachedContext(IOwinContext context, IKeycloakParameters options, bool preload = true) { - var newContext = new OidcDataManager(options); - OidcManagerCache[options.AuthenticationType + CachedContextPostfix] = newContext; + var newContext = new OidcDataManager(context, options); + var realmPrefix = GetRealmPrefix(context); + OidcManagerCache[realmPrefix + options.AuthenticationType + CachedContextPostfix] = newContext; if (preload) await newContext.ValidateCachedContextAsync(); return newContext; } + private static string GetRealmPrefix(IOwinContext context) + { + return MultiTenantRealmSelector != null ? MultiTenantRealmSelector(context) + "_": ""; + } + #endregion #region Metadata Handling @@ -135,7 +147,8 @@ public async Task TryRefreshMetadataAsync() public async Task RefreshMetadataAsync() { // Get Metadata from endpoint - var dataTask = HttpApiGet(MetadataEndpoint); + var metadataEndpoint = MetadataEndpoint; + var dataTask = HttpApiGet(metadataEndpoint); // Try to get the JSON metadata object JObject json; @@ -147,7 +160,7 @@ public async Task RefreshMetadataAsync() { // Fail on invalid JSON throw new Exception( - $"RefreshMetadataAsync: Metadata address returned invalid JSON object ('{MetadataEndpoint}')", + $"RefreshMetadataAsync: Metadata address returned invalid JSON object ('{metadataEndpoint}')", exception); } @@ -187,7 +200,7 @@ public async Task RefreshMetadataAsync() { // Fail on invalid URI or metadata throw new Exception( - $"RefreshMetadataAsync: Metadata address returned incomplete data ('{MetadataEndpoint}')", exception); + $"RefreshMetadataAsync: Metadata address returned incomplete data ('{metadataEndpoint}')", exception); } } diff --git a/src/KeycloakIdentityModel/packages.config b/src/KeycloakIdentityModel/packages.config index 1d46e63..c542e4c 100644 --- a/src/KeycloakIdentityModel/packages.config +++ b/src/KeycloakIdentityModel/packages.config @@ -1,6 +1,9 @@  + + + \ No newline at end of file diff --git a/src/Owin.Security.Keycloak/Configuration/KeycloakAuthenticationOptions.cs b/src/Owin.Security.Keycloak/Configuration/KeycloakAuthenticationOptions.cs index f691c11..afa127c 100644 --- a/src/Owin.Security.Keycloak/Configuration/KeycloakAuthenticationOptions.cs +++ b/src/Owin.Security.Keycloak/Configuration/KeycloakAuthenticationOptions.cs @@ -1,5 +1,6 @@ using System; using KeycloakIdentityModel.Models.Configuration; +using Microsoft.Owin; using Microsoft.Owin.Security; namespace Owin.Security.Keycloak @@ -27,6 +28,14 @@ public KeycloakAuthenticationOptions() /// public string Realm { get; set; } + /// + /// OPTIONAL: The MultiTenantRealmSelector is used to dynamically set the Realm for multi-tenant applications. + /// + /// + /// - The purpose is for multi-tenant platforms that require multiple Realms. + /// + public Func MultiTenantRealmSelector { get; set; } + /// /// The client ID to use for the application /// diff --git a/src/Owin.Security.Keycloak/Extensions/AppBuilderExtension.cs b/src/Owin.Security.Keycloak/Extensions/AppBuilderExtension.cs index c614a7c..1d6db85 100644 --- a/src/Owin.Security.Keycloak/Extensions/AppBuilderExtension.cs +++ b/src/Owin.Security.Keycloak/Extensions/AppBuilderExtension.cs @@ -1,4 +1,5 @@ -using Owin.Security.Keycloak.Middleware; +using KeycloakIdentityModel.Utilities; +using Owin.Security.Keycloak.Middleware; namespace Owin.Security.Keycloak { @@ -6,6 +7,7 @@ public static class AppBuilderExtension { public static IAppBuilder UseKeycloakAuthentication(this IAppBuilder app, KeycloakAuthenticationOptions options) { + OidcDataManager.MultiTenantRealmSelector = options.MultiTenantRealmSelector; app.Use(typeof (KeycloakAuthenticationMiddleware), app, options); return app; } diff --git a/src/Owin.Security.Keycloak/Middleware/KeycloakAuthenticationHandler.cs b/src/Owin.Security.Keycloak/Middleware/KeycloakAuthenticationHandler.cs index 9e24efb..c38410b 100644 --- a/src/Owin.Security.Keycloak/Middleware/KeycloakAuthenticationHandler.cs +++ b/src/Owin.Security.Keycloak/Middleware/KeycloakAuthenticationHandler.cs @@ -31,8 +31,8 @@ protected override async Task AuthenticateCoreAsync() try { var authResponse = new TokenResponse(bearerAuthArr[1], null, null); - var kcIdentity = await KeycloakIdentity.ConvertFromTokenResponseAsync(Options, authResponse); - var identity = await kcIdentity.ToClaimsIdentityAsync(); + var kcIdentity = await KeycloakIdentity.ConvertFromTokenResponseAsync(Context, Options, authResponse); + var identity = await kcIdentity.ToClaimsIdentityAsync(Context); SignInAsAuthentication(identity, null, Options.SignInAsAuthenticationType); return new AuthenticationTicket(identity, new AuthenticationProperties()); } @@ -54,10 +54,10 @@ public override async Task InvokeAsync() { // Check SignInAs identity for authentication update if (Context.Authentication.User.Identity.IsAuthenticated) - await ValidateSignInAsIdentities(); + await ValidateSignInAsIdentities(Context); // Check for valid callback URI - var callbackUri = await KeycloakIdentity.GenerateLoginCallbackUriAsync(Options, Request.Uri); + var callbackUri = await KeycloakIdentity.GenerateLoginCallbackUriAsync(Context, Options, Request.Uri); if (!Options.ForceBearerTokenAuth && Request.Uri.GetLeftPart(UriPartial.Path) == callbackUri.ToString()) { // Create authorization result from query @@ -77,8 +77,8 @@ stateData[Constants.CacheTypes.AuthenticationProperties] as AuthenticationProper // Process response var kcIdentity = - await KeycloakIdentity.ConvertFromAuthResponseAsync(Options, authResult, Request.Uri); - var identity = await kcIdentity.ToClaimsIdentityAsync(); + await KeycloakIdentity.ConvertFromAuthResponseAsync(Context, Options, authResult, Request.Uri); + var identity = await kcIdentity.ToClaimsIdentityAsync(Context); Context.Authentication.User.AddIdentity(identity); SignInAsAuthentication(identity, properties, Options.SignInAsAuthenticationType); @@ -185,11 +185,11 @@ private async Task ValidateSignInAsIdentities() { if (!origIdentity.HasClaim(Constants.ClaimTypes.AuthenticationType, Options.AuthenticationType)) continue; - var kcIdentity = await KeycloakIdentity.ConvertFromClaimsIdentityAsync(Options, origIdentity); + var kcIdentity = await KeycloakIdentity.ConvertFromClaimsIdentityAsync(Context, Options, origIdentity); if (!kcIdentity.IsTouched) continue; // Replace identity if expired - var identity = await kcIdentity.ToClaimsIdentityAsync(); + var identity = await kcIdentity.ToClaimsIdentityAsync(Context); Context.Authentication.User = new ClaimsPrincipal(identity); SignInAsAuthentication(identity, null, Options.SignInAsAuthenticationType); } @@ -247,7 +247,7 @@ private async Task LoginRedirectAsync(AuthenticationProperties properties) var state = Global.StateCache.CreateState(stateData); // Redirect response to login - Response.Redirect((await KeycloakIdentity.GenerateLoginUriAsync(Options, Request.Uri, state)).ToString()); + Response.Redirect((await KeycloakIdentity.GenerateLoginUriAsync(Context, Options, Request.Uri, state)).ToString()); } private async Task LogoutRedirectAsync() @@ -255,7 +255,7 @@ private async Task LogoutRedirectAsync() // Redirect response to logout Response.Redirect( (await - KeycloakIdentity.GenerateLogoutUriAsync(Options, Request.Uri)) + KeycloakIdentity.GenerateLogoutUriAsync(Context, Options, Request.Uri)) .ToString()); } diff --git a/src/Owin.Security.Keycloak/Middleware/KeycloakAuthenticationMiddleware.cs b/src/Owin.Security.Keycloak/Middleware/KeycloakAuthenticationMiddleware.cs index 2e8f4b1..5fa3aa6 100644 --- a/src/Owin.Security.Keycloak/Middleware/KeycloakAuthenticationMiddleware.cs +++ b/src/Owin.Security.Keycloak/Middleware/KeycloakAuthenticationMiddleware.cs @@ -36,7 +36,7 @@ private void ValidateOptions() // Verify required options if (Options.KeycloakUrl == null) ThrowOptionNotFound(nameof(Options.KeycloakUrl)); - if (Options.Realm == null) + if (Options.Realm == null && Options.MultiTenantRealmSelector == null) ThrowOptionNotFound(nameof(Options.Realm)); // Load web root path from config From 5300c5ba5b0a81b9b6644a2c1bdeb5e80d5ad92f Mon Sep 17 00:00:00 2001 From: "joel.thoms" Date: Thu, 25 Aug 2016 13:54:18 -0700 Subject: [PATCH 2/2] Bug fix --- .../Middleware/KeycloakAuthenticationHandler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Owin.Security.Keycloak/Middleware/KeycloakAuthenticationHandler.cs b/src/Owin.Security.Keycloak/Middleware/KeycloakAuthenticationHandler.cs index c38410b..7e9afe6 100644 --- a/src/Owin.Security.Keycloak/Middleware/KeycloakAuthenticationHandler.cs +++ b/src/Owin.Security.Keycloak/Middleware/KeycloakAuthenticationHandler.cs @@ -54,7 +54,7 @@ public override async Task InvokeAsync() { // Check SignInAs identity for authentication update if (Context.Authentication.User.Identity.IsAuthenticated) - await ValidateSignInAsIdentities(Context); + await ValidateSignInAsIdentities(); // Check for valid callback URI var callbackUri = await KeycloakIdentity.GenerateLoginCallbackUriAsync(Context, Options, Request.Uri);