diff --git a/changelog.md b/changelog.md index 8ea4ff1..a4b7b39 100644 --- a/changelog.md +++ b/changelog.md @@ -10,13 +10,6 @@ If you have questions or need help, please reach out to us at support@stormpath. Follow the [migration guide](migrating.md) to understand how to migrate an application step-by-step. -### Coming soon - -These features don't work yet, but are coming in a future RC. - -* Client Credentials (API key/secret) authentication -* Updating user profile or custom fields (reading works, no way to save currently) - ### Stormpath features that will not migrate See the Compatibility Matrix on the [Stormpath-Okta Customer FAQ](https://stormpath.com/oktaplusstormpath) for a complete list of features that are not being migrated. The relevant points for this library are: @@ -26,12 +19,18 @@ See the Compatibility Matrix on the [Stormpath-Okta Customer FAQ](https://stormp * Custom Data is only be available on account resources. * The Verification Success Email, Welcome Email, and Password Reset Success Email workflows are not supported. +## Version 4.0.0-RC3 + +### Breaking changes + +* The Client Credentials grant works, but it is handled differently in Okta than it was at Stormpath. The API key ID and secret are stored in the user profile, and are verified locally by this middleware code. This feature is intended to help our customers migrate, but won't be how Okta supports API key management going forward. If you're using the API key management features of Stormpath heavily, please reach out to support@stormpath.com and let us know so we can assist. ## Version 4.0.0-RC2 ### Breaking changes * Authorizing (using attributes in ASP.NET or handlers in ASP.NET Core) by Group `href` is no longer possible. Authorizing by Group name still works. +* Any `OrganizationNameKey` value that is set in a pre-handler is not honored. #### Social login diff --git a/src/Stormpath.Owin.Middleware/AccessTokenValidator.cs b/src/Stormpath.Owin.Middleware/AccessTokenValidator.cs index af05eba..7bda012 100644 --- a/src/Stormpath.Owin.Middleware/AccessTokenValidator.cs +++ b/src/Stormpath.Owin.Middleware/AccessTokenValidator.cs @@ -25,6 +25,17 @@ public AccessTokenValidator( public Task ValidateAsync(string token, CancellationToken cancellationToken) { + // Try to validate the token as a token issued by the Client Credentials flow (OauthRoute) + var orphanValidator = new OrphanAccessTokenValidator( + _configuration.Application.Id, + _configuration.OktaEnvironment.ClientId, + _configuration.OktaEnvironment.ClientSecret); + var orphanValidationResult = orphanValidator.ValidateAsync(token); + if (orphanValidationResult != TokenIntrospectionResult.Invalid) + { + return Task.FromResult(orphanValidationResult); + } + if (_configuration.Web.Oauth2.Password.ValidationStrategy == WebOauth2TokenValidationStrategy.Stormpath) { var remoteValidator = new RemoteTokenValidator( diff --git a/src/Stormpath.Owin.Middleware/ConstantTimeComparer.cs b/src/Stormpath.Owin.Middleware/ConstantTimeComparer.cs new file mode 100644 index 0000000..9694de8 --- /dev/null +++ b/src/Stormpath.Owin.Middleware/ConstantTimeComparer.cs @@ -0,0 +1,39 @@ +using System.Text; + +namespace Stormpath.Owin.Middleware +{ + public static class ConstantTimeComparer + { + public static bool Equals(string x, string y) + { + var xIsNull = x == null; + var yIsNull = y == null; + + if (xIsNull && yIsNull) + { + return true; + } + + if ((xIsNull && !yIsNull) || (!xIsNull && yIsNull)) + { + return false; + } + + if (x.Length != y.Length) + { + return false; + } + + byte[] decodedCryptoBytes = Encoding.ASCII.GetBytes(x); + byte[] decodedSignatureBytes = Encoding.ASCII.GetBytes(y); + + byte result = 0; + for (int i = 0; i < x.Length; i++) + { + result |= (byte)(decodedCryptoBytes[i] ^ decodedSignatureBytes[i]); + } + + return result == 0; + } + } +} diff --git a/src/Stormpath.Owin.Middleware/GrantResult.cs b/src/Stormpath.Owin.Middleware/GrantResult.cs index 474215e..2e5bb6a 100644 --- a/src/Stormpath.Owin.Middleware/GrantResult.cs +++ b/src/Stormpath.Owin.Middleware/GrantResult.cs @@ -1,4 +1,5 @@ using Newtonsoft.Json; +using Stormpath.Owin.Middleware.Okta; namespace Stormpath.Owin.Middleware { diff --git a/src/Stormpath.Owin.Middleware/LoginExecutor.cs b/src/Stormpath.Owin.Middleware/LoginExecutor.cs index f18fc29..cecce60 100644 --- a/src/Stormpath.Owin.Middleware/LoginExecutor.cs +++ b/src/Stormpath.Owin.Middleware/LoginExecutor.cs @@ -14,16 +14,19 @@ // limitations under the License. // -using System; -using System.Threading; -using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Stormpath.Configuration.Abstractions.Immutable; +using Microsoft.IdentityModel.Tokens; using Stormpath.Owin.Abstractions; using Stormpath.Owin.Abstractions.Configuration; using Stormpath.Owin.Middleware.Internal; +using Stormpath.Owin.Middleware.Model.Error; using Stormpath.Owin.Middleware.Okta; +using System; using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; +using System.Threading; +using System.Threading.Tasks; namespace Stormpath.Owin.Middleware { @@ -48,7 +51,7 @@ public LoginExecutor( _logger = logger; } - public async Task PasswordGrantAsync( + public async Task<(GrantResult GrantResult, User User)> PasswordGrantAsync( IOwinEnvironment environment, Func errorHandler, string login, @@ -70,7 +73,7 @@ public async Task PasswordGrantAsync( ? "An error has occurred. Please try again." : preLoginHandlerContext.Result.ErrorMessage; await errorHandler(message, cancellationToken); - return null; + return (null, null); } } @@ -93,10 +96,79 @@ public async Task PasswordGrantAsync( if (!validationResult.Active) { _logger.LogWarning("The user's token was invalid."); - return null; + return (null, null); } - return grantResult; + var user = await UserHelper.GetUserFromAccessTokenAsync(_oktaClient, grantResult.AccessToken, _logger, cancellationToken); + + return (grantResult, user); + } + + public async Task ClientCredentialsGrantAsync( + IOwinEnvironment environment, + Func errorHandler, + string id, + string userId, + CancellationToken cancellationToken) + { + var preLoginHandlerContext = new PreLoginContext(environment) + { + Login = id + }; + + await _handlers.PreLoginHandler(preLoginHandlerContext, cancellationToken); + + if (preLoginHandlerContext.Result != null) + { + if (!preLoginHandlerContext.Result.Success) + { + var message = string.IsNullOrEmpty(preLoginHandlerContext.Result.ErrorMessage) + ? "An error has occurred. Please try again." + : preLoginHandlerContext.Result.ErrorMessage; + await errorHandler(new BadRequest(message), cancellationToken); + return null; + } + } + + var ttl = _configuration.Web.Oauth2.Client_Credentials.AccessToken.Ttl ?? 3600; + + return new GrantResult + { + AccessToken = BuildClientCredentialsAccessToken(id, userId, ttl), + TokenType = "Bearer", + ExpiresIn = ttl, + // Scope = TODO + }; + } + + private string BuildClientCredentialsAccessToken(string id, string userId, int timeToLive) + { + var signingKey = _configuration.OktaEnvironment.ClientSecret; + + var signingCredentials = new Microsoft.IdentityModel.Tokens.SigningCredentials( + new SymmetricSecurityKey(Encoding.ASCII.GetBytes(signingKey)), SecurityAlgorithms.HmacSha256); + + var now = DateTime.UtcNow; + + var claims = new[] + { + new Claim("sub", id), + new Claim("jti", Guid.NewGuid().ToString()), + new Claim("iat", ((long)((now - Cookies.Epoch).TotalSeconds)).ToString(), ClaimValueTypes.Integer64), + new Claim("cid", _configuration.OktaEnvironment.ClientId), + new Claim("uid", userId) + }; + + var jwt = new JwtSecurityToken( + claims: claims, + issuer: _configuration.Application.Id, + expires: now + TimeSpan.FromSeconds(timeToLive), + notBefore: DateTime.UtcNow, + signingCredentials: signingCredentials); + // TODO audience + // TODO scope + + return new JwtSecurityTokenHandler().WriteToken(jwt); } // TODO restore @@ -112,14 +184,15 @@ public async Task PasswordGrantAsync( public async Task HandlePostLoginAsync( IOwinEnvironment context, GrantResult grantResult, + User user, CancellationToken cancellationToken) { - var stormpathCompatibleUser = await UserHelper.GetUserFromAccessTokenAsync(_oktaClient, grantResult.AccessToken, _logger, cancellationToken); + var stormpathCompatibleUser = new CompatibleOktaAccount(user); var postLoginHandlerContext = new PostLoginContext(context, stormpathCompatibleUser); await _handlers.PostLoginHandler(postLoginHandlerContext, cancellationToken); - //Save the custom redirect URI from the handler, if any + // Save the custom redirect URI from the handler, if any _nextUriFromPostHandler = postLoginHandlerContext.Result?.RedirectUri; // Add Stormpath cookies diff --git a/src/Stormpath.Owin.Middleware/Okta/ApiKeyResolver.cs b/src/Stormpath.Owin.Middleware/Okta/ApiKeyResolver.cs new file mode 100644 index 0000000..fee930b --- /dev/null +++ b/src/Stormpath.Owin.Middleware/Okta/ApiKeyResolver.cs @@ -0,0 +1,36 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Stormpath.Owin.Middleware.Okta +{ + public class ApiKeyResolver + { + private readonly IOktaClient _oktaClient; + + public ApiKeyResolver(IOktaClient oktaClient) + { + _oktaClient = oktaClient; + } + + public async Task LookupApiKeyIdAsync(string id, string secret, CancellationToken cancellationToken) + { + var apiKey = await _oktaClient.GetApiKeyAsync(id, cancellationToken); + + var validKey = + apiKey != null && + apiKey.Status.Equals("enabled", StringComparison.OrdinalIgnoreCase) && + ConstantTimeComparer.Equals(apiKey.Secret, secret); + + if (!validKey) return null; + + var validAccount = + apiKey.User != null && + apiKey.User.Status.Equals("active", StringComparison.OrdinalIgnoreCase); + + if (!validAccount) return null; + + return apiKey; + } + } +} diff --git a/src/Stormpath.Owin.Middleware/Okta/IOktaClient.cs b/src/Stormpath.Owin.Middleware/Okta/IOktaClient.cs index 9d6090d..a3e4164 100644 --- a/src/Stormpath.Owin.Middleware/Okta/IOktaClient.cs +++ b/src/Stormpath.Owin.Middleware/Okta/IOktaClient.cs @@ -101,5 +101,7 @@ Task SendPasswordResetEmailAsync( Task GetIdentityProvidersAsync(CancellationToken ct); Task GetGroupsForUserIdAsync(string userId, CancellationToken cancellationToken); + + Task GetApiKeyAsync(string apiKeyId, CancellationToken cancellationToken); } } \ No newline at end of file diff --git a/src/Stormpath.Owin.Middleware/Okta/OauthApiError.cs b/src/Stormpath.Owin.Middleware/Okta/OauthApiError.cs new file mode 100644 index 0000000..4388b71 --- /dev/null +++ b/src/Stormpath.Owin.Middleware/Okta/OauthApiError.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace Stormpath.Owin.Middleware.Okta +{ + public class OauthApiError + { + public string Error { get; set; } + + [JsonProperty("error_description")] + public string Description { get; set; } + } +} diff --git a/src/Stormpath.Owin.Middleware/Okta/OktaClient.cs b/src/Stormpath.Owin.Middleware/Okta/OktaClient.cs index 09de1a0..8abf863 100644 --- a/src/Stormpath.Owin.Middleware/Okta/OktaClient.cs +++ b/src/Stormpath.Owin.Middleware/Okta/OktaClient.cs @@ -9,6 +9,7 @@ using Microsoft.Extensions.Logging; using Newtonsoft.Json; using Stormpath.Owin.Middleware.Internal; +using System.Linq; namespace Stormpath.Owin.Middleware.Okta { @@ -295,8 +296,28 @@ public Task PostPasswordGrantAsync( }; request.Content = new FormUrlEncodedContent(parameters); + var exceptionFormatter = new Func((statusCode, body) => + { + _logger.LogWarning($"{statusCode} {body}"); + + try + { + var deserialized = JsonConvert.DeserializeObject(body); + var isInvalidGrantError = deserialized?.Error?.Equals("invalid_grant") ?? false; + + if (!isInvalidGrantError) return DefaultExceptionFormatter(statusCode, body); + + return new InvalidOperationException("Invalid username or password."); + } + catch (Exception ex) + { + _logger.LogError(1005, ex, "Error while formatting error response"); + return DefaultExceptionFormatter(statusCode, body); + } + }); + _logger.LogTrace($"Executing password grant flow for subject {username}"); - return SendAsync(request, cancellationToken); + return SendAsync(request, cancellationToken, exceptionFormatter); } public Task PostRefreshGrantAsync( @@ -457,5 +478,57 @@ public Task GetIdentityProvidersAsync(CancellationToken ct) public Task GetGroupsForUserIdAsync(string userId, CancellationToken cancellationToken) => GetResource($"{ApiPrefix}/users/{userId}/groups", cancellationToken); + + private const string ProfileAttributeDoesNotExist = "E0000031"; + + public async Task GetApiKeyAsync(string apiKeyId, CancellationToken cancellationToken) + { + User foundUser = null; + string foundKeypair = null; + + for (var i = 0; i < 10; i++) + { + try + { + var foundUsers = await SearchUsersAsync($"profile.stormpathApiKey_{i} sw \"{apiKeyId}\"", cancellationToken); + + foundUser = foundUsers?.FirstOrDefault(); + + if (foundUser == null) continue; + + foundUser.Profile.TryGetValue($"stormpathApiKey_{i}", out var rawValue); + foundKeypair = rawValue?.ToString(); + + var keypairTokens = foundKeypair?.Split(new[] { ':' }, StringSplitOptions.RemoveEmptyEntries); + var valid = keypairTokens?.Length == 2; + + if (!valid) continue; + + return new ShimApiKey + { + Id = keypairTokens[0], + Secret = keypairTokens[1], + Status = "ENABLED", + User = foundUser + }; + } + catch (OktaException oex) + { + object rawCode = null; + oex?.Body?.TryGetValue("errorCode", out rawCode); + var code = rawCode?.ToString(); + if (string.IsNullOrEmpty(code)) throw; + + // Code E0000031 means "the profile attribute doesn't exist" + if (code.Equals(ProfileAttributeDoesNotExist)) + { + _logger.LogWarning($"The profile attribute 'profile.stormpathApiKey_{i}' should be added to your Universal Directory configuration."); + continue; + } + } + } + + return null; + } } } diff --git a/src/Stormpath.Owin.Middleware/Okta/ShimApiKey.cs b/src/Stormpath.Owin.Middleware/Okta/ShimApiKey.cs new file mode 100644 index 0000000..5ce729f --- /dev/null +++ b/src/Stormpath.Owin.Middleware/Okta/ShimApiKey.cs @@ -0,0 +1,13 @@ +namespace Stormpath.Owin.Middleware.Okta +{ + public class ShimApiKey + { + public string Id { get; set; } + + public string Secret { get; set; } + + public string Status { get; set; } + + public User User { get; set; } + } +} diff --git a/src/Stormpath.Owin.Middleware/OrphanAccessTokenValidator.cs b/src/Stormpath.Owin.Middleware/OrphanAccessTokenValidator.cs new file mode 100644 index 0000000..69240ec --- /dev/null +++ b/src/Stormpath.Owin.Middleware/OrphanAccessTokenValidator.cs @@ -0,0 +1,97 @@ +using Microsoft.IdentityModel.Tokens; +using Stormpath.Owin.Middleware.Okta; +using System; +using System.IdentityModel.Tokens.Jwt; +using System.Linq; +using System.Text; + +namespace Stormpath.Owin.Middleware +{ + /// + /// Validates tokens issued locally by this middleware code. + /// This flow is used by the Client Credentials grant type. + /// + public sealed class OrphanAccessTokenValidator + { + private readonly string _applicationId; + private readonly string _clientId; + private readonly string _clientSecret; + + public OrphanAccessTokenValidator( + string applicationId, + string clientId, + string clientSecret) + { + _applicationId = applicationId; + _clientId = clientId; + _clientSecret = clientSecret; + } + + public JwtSecurityToken ValidateSecurityToken(string token) + { + // TODO need ITs for this + + var signingKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(_clientSecret)); + + var param = new TokenValidationParameters() + { + ValidateIssuer = true, + ValidIssuer = _applicationId, + ValidateLifetime = true, + RequireExpirationTime = true, + RequireSignedTokens = true, + ValidateIssuerSigningKey = true, + IssuerSigningKey = signingKey, + + // TODO what will the standard audience be? + ValidateAudience = false + }; + + try + { + new JwtSecurityTokenHandler().ValidateToken(token, param, out SecurityToken securityToken); + + return securityToken as JwtSecurityToken; + } + catch (Exception) + { + // Token is invalid + return null; + } + } + + public TokenIntrospectionResult ValidateAsync(string token) + { + var decodedToken = ValidateSecurityToken(token); + if (decodedToken == null) return TokenIntrospectionResult.Invalid; + + bool hasClientIdClaim = decodedToken.Payload.TryGetValue("cid", out var rawCid); + if (!hasClientIdClaim) return TokenIntrospectionResult.Invalid; + + bool clientIdMatches = rawCid?.ToString().Equals(_clientId) ?? false; + if (!clientIdMatches) return TokenIntrospectionResult.Invalid; + + decodedToken.Payload.TryGetValue("uid", out var rawUid); + + decodedToken.Payload.TryGetValue("scp", out var rawScope); + var scopesAsArray = (rawScope as Newtonsoft.Json.Linq.JArray)?.Select(t => t?.ToString()) ?? new[] { string.Empty }; + + return new TokenIntrospectionResult + { + Active = true, + Aud = decodedToken.Payload.Aud, + ClientId = rawCid.ToString(), + Exp = decodedToken.Payload.Exp, + Iat = decodedToken.Payload.Iat, + Iss = decodedToken.Payload.Iss, + Jti = decodedToken.Payload.Jti, + Scope = string.Join(" ", scopesAsArray), + Sub = decodedToken.Payload.Sub, + TokenType = "Bearer", + Uid = rawUid?.ToString(), + Username = decodedToken.Payload.Sub + }; + } + + } +} diff --git a/src/Stormpath.Owin.Middleware/RegisterExecutor.cs b/src/Stormpath.Owin.Middleware/RegisterExecutor.cs index b3fd6c3..0b67253 100644 --- a/src/Stormpath.Owin.Middleware/RegisterExecutor.cs +++ b/src/Stormpath.Owin.Middleware/RegisterExecutor.cs @@ -175,14 +175,14 @@ private async Task HandleAutologinAsync( CancellationToken cancellationToken) { var loginExecutor = new LoginExecutor(_configuration, _handlers, _oktaClient, _logger); - var loginResult = await loginExecutor.PasswordGrantAsync( + var (grantResult, user) = await loginExecutor.PasswordGrantAsync( environment, errorHandler, postModel.Email, postModel.Password, cancellationToken); - await loginExecutor.HandlePostLoginAsync(environment, loginResult, cancellationToken); + await loginExecutor.HandlePostLoginAsync(environment, grantResult, user, cancellationToken); var parsedStateToken = new StateTokenParser( _configuration.Application.Id, diff --git a/src/Stormpath.Owin.Middleware/Route/LoginRoute.cs b/src/Stormpath.Owin.Middleware/Route/LoginRoute.cs index af61771..7731f4c 100644 --- a/src/Stormpath.Owin.Middleware/Route/LoginRoute.cs +++ b/src/Stormpath.Owin.Middleware/Route/LoginRoute.cs @@ -94,7 +94,7 @@ protected override async Task PostHtmlAsync(IOwinEnvironment context, Cont try { - var grantResult = await executor.PasswordGrantAsync( + var (grantResult, user) = await executor.PasswordGrantAsync( context, htmlErrorHandler, model.Login, @@ -106,7 +106,7 @@ protected override async Task PostHtmlAsync(IOwinEnvironment context, Cont return true; // The error handler was invoked } - await executor.HandlePostLoginAsync(context, grantResult, cancellationToken); + await executor.HandlePostLoginAsync(context, grantResult, user, cancellationToken); } catch (Exception ex) { @@ -148,7 +148,7 @@ protected override async Task PostJsonAsync(IOwinEnvironment context, Cont var executor = new LoginExecutor(_configuration, _handlers, _oktaClient, _logger); - var grantResult = await executor.PasswordGrantAsync( + var (grantResult, user) = await executor.PasswordGrantAsync( context, jsonErrorHandler, model.Login, @@ -160,7 +160,7 @@ protected override async Task PostJsonAsync(IOwinEnvironment context, Cont return true; // The error handler was invoked } - var account = await executor.HandlePostLoginAsync(context, grantResult, cancellationToken); + var account = await executor.HandlePostLoginAsync(context, grantResult, user, cancellationToken); var sanitizer = new AccountResponseSanitizer(); var responseModel = new diff --git a/src/Stormpath.Owin.Middleware/Route/Oauth2Route.cs b/src/Stormpath.Owin.Middleware/Route/Oauth2Route.cs index 74b7112..b454b2e 100644 --- a/src/Stormpath.Owin.Middleware/Route/Oauth2Route.cs +++ b/src/Stormpath.Owin.Middleware/Route/Oauth2Route.cs @@ -61,6 +61,13 @@ protected override async Task PostAsync(IOwinEnvironment context, ContentN try { + if (grantType.Equals("client_credentials", StringComparison.OrdinalIgnoreCase) + && _configuration.Web.Oauth2.Client_Credentials.Enabled) + { + await ExecuteClientCredentialsFlow(context, _oktaClient, cancellationToken); + return true; + } + if (grantType.Equals("password", StringComparison.OrdinalIgnoreCase) && _configuration.Web.Oauth2.Password.Enabled) { @@ -106,14 +113,14 @@ private async Task ExecutePasswordFlow(IOwinEnvironment context, string us var jsonErrorHandler = new Func((message, ct) => Error.Create(context, new BadRequest(message), ct)); - var grantResult = await executor.PasswordGrantAsync( + var (grantResult, user) = await executor.PasswordGrantAsync( context, jsonErrorHandler, username, password, cancellationToken); - await executor.HandlePostLoginAsync(context, grantResult, cancellationToken); + await executor.HandlePostLoginAsync(context, grantResult, user, cancellationToken); var sanitizer = new GrantResultResponseSanitizer(); return await JsonResponse.Ok(context, sanitizer.SanitizeResponseWithRefreshToken(grantResult)).ConfigureAwait(false); @@ -131,5 +138,46 @@ private async Task ExecuteRefreshFlow(IOwinEnvironment context, string ref var sanitizer = new GrantResultResponseSanitizer(); return await JsonResponse.Ok(context, sanitizer.SanitizeResponseWithRefreshToken(grantResult)).ConfigureAwait(false); } + + private async Task ExecuteClientCredentialsFlow(IOwinEnvironment context, IOktaClient oktaClient, CancellationToken cancellationToken) + { + var jsonErrorHandler = new Func((err, ct) + => Error.Create(context, err, ct)); + + var basicHeaderParser = new BasicAuthenticationParser(context.Request.Headers.GetString("Authorization"), _logger); + if (!basicHeaderParser.IsValid) + { + await jsonErrorHandler(new OauthInvalidRequest(), cancellationToken); + return true; + } + + var apiKeyResolver = new ApiKeyResolver(oktaClient); + var apiKey = await apiKeyResolver.LookupApiKeyIdAsync(basicHeaderParser.Username, basicHeaderParser.Password, cancellationToken); + + if (apiKey == null) + { + await jsonErrorHandler(new OauthInvalidClient(), cancellationToken); + return true; + } + + var executor = new LoginExecutor(_configuration, _handlers, _oktaClient, _logger); + + var tokenResult = await executor.ClientCredentialsGrantAsync( + context, + jsonErrorHandler, + basicHeaderParser.Username, + apiKey.User.Id, + cancellationToken); + + if (tokenResult == null) + { + return true; // Some error occurred and the handler was invoked + } + + await executor.HandlePostLoginAsync(context, tokenResult, apiKey.User, cancellationToken); + + var sanitizer = new GrantResultResponseSanitizer(); + return await JsonResponse.Ok(context, sanitizer.SanitizeResponseWithoutRefreshToken(tokenResult)).ConfigureAwait(false); + } } } diff --git a/src/Stormpath.Owin.Middleware/Route/StormpathCallbackRoute.cs b/src/Stormpath.Owin.Middleware/Route/StormpathCallbackRoute.cs index ec505de..55ebea3 100644 --- a/src/Stormpath.Owin.Middleware/Route/StormpathCallbackRoute.cs +++ b/src/Stormpath.Owin.Middleware/Route/StormpathCallbackRoute.cs @@ -1,5 +1,6 @@ using Stormpath.Owin.Abstractions; using Stormpath.Owin.Middleware.Internal; +using Stormpath.Owin.Middleware.Okta; using System; using System.Threading; using System.Threading.Tasks; @@ -38,17 +39,20 @@ protected override async Task GetAsync( _configuration.AbsoluteCallbackUri, cancellationToken); - return await LoginAndRedirectAsync(context, grantResult, parsedStateToken.Path, cancellationToken); + var user = await UserHelper.GetUserFromAccessTokenAsync(_oktaClient, grantResult.AccessToken, _logger, cancellationToken); + + return await LoginAndRedirectAsync(context, grantResult, user, parsedStateToken.Path, cancellationToken); } private async Task LoginAndRedirectAsync( IOwinEnvironment context, GrantResult grantResult, + User user, string nextPath, CancellationToken cancellationToken) { var executor = new LoginExecutor(_configuration, _handlers, _oktaClient, _logger); - await executor.HandlePostLoginAsync(context, grantResult, cancellationToken); + await executor.HandlePostLoginAsync(context, grantResult, user, cancellationToken); // TODO determine whether this is a new account or not diff --git a/src/Stormpath.Owin.Middleware/Stormpath.Owin.Middleware.csproj b/src/Stormpath.Owin.Middleware/Stormpath.Owin.Middleware.csproj index 7e5d9ee..5ddde4f 100644 --- a/src/Stormpath.Owin.Middleware/Stormpath.Owin.Middleware.csproj +++ b/src/Stormpath.Owin.Middleware/Stormpath.Owin.Middleware.csproj @@ -3,7 +3,7 @@ Stormpath OWIN middleware library (c) 2016 Stormpath, Inc. - 4.0.0-rc2 + 4.0.0-rc3 Nate Barbettini net451;netstandard1.4 $(NoWarn);CS1591;CS0618 @@ -15,7 +15,6 @@ https://github.com/stormpath/stormpath-dotnet-owin-middleware https://github.com/stormpath/stormpath-dotnet-owin-middleware/blob/master/LICENSE https://github.com/stormpath/stormpath-dotnet-owin-middleware - $(PackageTargetFallback);dotnet @@ -26,8 +25,8 @@ - - + + diff --git a/src/Stormpath.Owin.Middleware/StormpathMiddleware.GetUser.cs b/src/Stormpath.Owin.Middleware/StormpathMiddleware.GetUser.cs index dd1311f..32a8fda 100644 --- a/src/Stormpath.Owin.Middleware/StormpathMiddleware.GetUser.cs +++ b/src/Stormpath.Owin.Middleware/StormpathMiddleware.GetUser.cs @@ -48,10 +48,39 @@ private async Task GetUserAsync(IOwinEnvironment context return cookieAuthenticationResult; } + var apiAuthenticationResult = await TryBasicAuthenticationAsync(context, oktaClient); + if (apiAuthenticationResult != null) + { + context.Request[OwinKeys.StormpathUserScheme] = RequestAuthenticationScheme.ApiCredentials; + return apiAuthenticationResult; + } + logger.LogTrace("No user found on request", nameof(GetUserAsync)); return null; } + private Task TryBasicAuthenticationAsync(IOwinEnvironment context, IOktaClient client) + { + var basicHeaderParser = new BasicAuthenticationParser( + context.Request.Headers.GetString("Authorization"), + logger); + if (!basicHeaderParser.IsValid) + { + return Task.FromResult(null); + } + + try + { + logger.LogInformation("Using Basic header to authenticate request"); + return ValidateApiCredentialsAsync(context, client, basicHeaderParser.Username, basicHeaderParser.Password); + } + catch (Exception ex) + { + logger.LogWarning(1001, ex, "Error during TryBasicAuthenticationAsync"); + return Task.FromResult(null); + } + } + private Task TryBearerAuthenticationAsync(IOwinEnvironment context, IOktaClient oktaClient) { var bearerHeaderParser = new BearerAuthenticationParser( @@ -151,7 +180,7 @@ private async Task ValidateAccessTokenAsync(IOwinEnviron ICompatibleOktaAccount account = null; try { - account = await UserHelper.GetUserFromAccessTokenAsync(oktaClient, accessTokenJwt, logger, context.CancellationToken); + account = await UserHelper.GetAccountFromAccessTokenAsync(oktaClient, accessTokenJwt, logger, context.CancellationToken); } catch (Exception ex) { @@ -183,7 +212,7 @@ private async Task RefreshAccessTokenAsync(IOwinEnvironm ICompatibleOktaAccount account = null; try { - account = await UserHelper.GetUserFromAccessTokenAsync(oktaClient, grantResult.AccessToken, logger, context.CancellationToken); + account = await UserHelper.GetAccountFromAccessTokenAsync(oktaClient, grantResult.AccessToken, logger, context.CancellationToken); } catch (Exception ex) { @@ -196,5 +225,23 @@ private async Task RefreshAccessTokenAsync(IOwinEnvironm return account; } + + private async Task ValidateApiCredentialsAsync( + IOwinEnvironment context, + IOktaClient client, + string id, + string secret) + { + var apiKeyResolver = new ApiKeyResolver(client); + var apiKey = await apiKeyResolver.LookupApiKeyIdAsync(id, secret, context.CancellationToken); + + if (apiKey == null) + { + logger.LogInformation($"API key with ID {id} and matching secret was not found", nameof(ValidateApiCredentialsAsync)); + return null; + } + + return new CompatibleOktaAccount(apiKey.User); + } } } diff --git a/src/Stormpath.Owin.Middleware/UserHelper.cs b/src/Stormpath.Owin.Middleware/UserHelper.cs index c463dae..5adef9c 100644 --- a/src/Stormpath.Owin.Middleware/UserHelper.cs +++ b/src/Stormpath.Owin.Middleware/UserHelper.cs @@ -10,7 +10,7 @@ namespace Stormpath.Owin.Middleware { internal static class UserHelper { - public static async Task GetUserFromAccessTokenAsync( + public static async Task GetUserFromAccessTokenAsync( IOktaClient oktaClient, string accessToken, ILogger logger, @@ -22,8 +22,17 @@ public static async Task GetUserFromAccessTokenAsync( throw new Exception("Could not get user information"); } - var oktaUser = await oktaClient.GetUserAsync(rawUid.ToString(), cancellationToken); - return new CompatibleOktaAccount(oktaUser); + return await oktaClient.GetUserAsync(rawUid.ToString(), cancellationToken); + } + + public static async Task GetAccountFromAccessTokenAsync( + IOktaClient oktaClient, + string accessToken, + ILogger logger, + CancellationToken cancellationToken) + { + return new CompatibleOktaAccount( + await GetUserFromAccessTokenAsync(oktaClient, accessToken, logger, cancellationToken)); } } }