/// <summary> /// Tries to find a secret on the environment that can be used for authentication /// </summary> /// <param name="environment">The environment.</param> /// <returns> /// A parsed secret /// </returns> public async Task<ParsedSecret> ParseAsync(IDictionary<string, object> environment) { Logger.Debug("Start parsing for secret in post body"); var context = new OwinContext(environment); var body = await context.ReadRequestFormAsync(); if (body != null) { var id = body.Get("client_id"); var secret = body.Get("client_secret"); if (id.IsPresent() && secret.IsPresent()) { var parsedSecret = new ParsedSecret { Id = id, Credential = secret, Type = Constants.ParsedSecretTypes.SharedSecret }; return parsedSecret; } } Logger.Debug("No secet in post body found"); return null; }
/// <summary> /// Validates a secret /// </summary> /// <param name="secrets">The stored secrets.</param> /// <param name="parsedSecret">The received secret.</param> /// <returns> /// A validation result /// </returns> /// <exception cref="System.ArgumentException">ParsedSecret.Credential is not an X509 Certificate</exception> public Task<SecretValidationResult> ValidateAsync(IEnumerable<Secret> secrets, ParsedSecret parsedSecret) { var fail = Task.FromResult(new SecretValidationResult { Success = false }); var success = Task.FromResult(new SecretValidationResult { Success = true }); if (parsedSecret.Type != Constants.ParsedSecretTypes.X509Certificate) { return fail; } var cert = parsedSecret.Credential as X509Certificate2; if (cert == null) { throw new ArgumentException("ParsedSecret.Credential is not an X509 Certificate"); } var thumbprint = cert.Thumbprint; foreach (var secret in secrets) { if (secret.Type == Constants.SecretTypes.X509CertificateThumbprint) { if (TimeConstantComparer.IsEqual(thumbprint.ToLowerInvariant(), secret.Value.ToLowerInvariant())) { return success; } } } return fail; }
public async Task Valid_Multiple_Secrets_No_Protection() { var clientId = "multiple_secrets_no_protection"; var client = await _clients.FindClientByIdAsync(clientId); var secret = new ParsedSecret { Id = clientId, Credential = "secret", Type = Constants.ParsedSecretTypes.SharedSecret }; var result = await _validator.ValidateAsync(client.ClientSecrets, secret); result.Success.Should().BeTrue(); secret.Credential = "foobar"; result = await _validator.ValidateAsync(client.ClientSecrets, secret); result.Success.Should().BeTrue(); secret.Credential = "quux"; result = await _validator.ValidateAsync(client.ClientSecrets, secret); result.Success.Should().BeTrue(); secret.Credential = "notexpired"; result = await _validator.ValidateAsync(client.ClientSecrets, secret); result.Success.Should().BeTrue(); }
public async Task<SecretValidationResult> ValidateAsync(ParsedSecret parsedSecret, IEnumerable<Secret> secrets) { var expiredSecrets = secrets.Where(s => s.Expiration.HasExpired()); if (expiredSecrets.Any()) { expiredSecrets.ToList().ForEach( ex => Logger.InfoFormat("Secret [{0}] is expired", ex.Description ?? "no description")); } var currentSecrets = secrets.Where(s => !s.Expiration.HasExpired()); // see if a registered validator can validate the secret foreach (var validator in _validators) { var secretValidationResult = await validator.ValidateAsync(currentSecrets, parsedSecret); if (secretValidationResult.Success) { Logger.DebugFormat("Secret validator success: {0}", validator.GetType().Name); return secretValidationResult; } } Logger.Info("Secret validators could not validate secret"); return new SecretValidationResult { Success = false }; }
public async Task Invalid_Certificate_Thumbprint() { var clientId = "certificate_invalid"; var client = await _clients.FindClientByIdAsync(clientId); var secret = new ParsedSecret { Id = clientId, Credential = TestCert.Load(), Type = Constants.ParsedSecretTypes.X509Certificate }; var result = await _thumbprintValidator.ValidateAsync(client.ClientSecrets, secret); result.Success.Should().BeFalse(); }
public async Task Null_Certificate_Should_Throw() { var clientId = "certificate_valid"; var client = await _clients.FindClientByIdAsync(clientId); var secret = new ParsedSecret { Id = clientId, Credential = null, Type = Constants.ParsedSecretTypes.X509Certificate }; Func<Task> act = () => _thumbprintValidator.ValidateAsync(client.ClientSecrets, secret); act.ShouldThrow<ArgumentException>(); }
public async Task Valid_Single_Secret() { var clientId = "single_secret_no_protection_no_expiration"; var client = await _clients.FindClientByIdAsync(clientId); var secret = new ParsedSecret { Id = clientId, Credential = "secret", Type = Constants.ParsedSecretTypes.SharedSecret }; var result = await _validator.ValidateAsync(client.ClientSecrets, secret); result.Success.Should().BeTrue(); }
public async Task Invalid_Credential_Type() { var clientId = "single_secret_no_protection_no_expiration"; var client = await _clients.FindClientByIdAsync(clientId); var secret = new ParsedSecret { Id = clientId, Credential = "secret", Type = "invalid" }; var result = await _validator.ValidateAsync(client.ClientSecrets, secret); result.Success.Should().BeFalse(); }
/// <summary> /// Validates a secret /// </summary> /// <param name="secrets">The stored secrets.</param> /// <param name="parsedSecret">The received secret.</param> /// <returns> /// A validation result /// </returns> /// <exception cref="System.ArgumentException">id or credential is missing.</exception> public Task<SecretValidationResult> ValidateAsync(IEnumerable<Secret> secrets, ParsedSecret parsedSecret) { var fail = Task.FromResult(new SecretValidationResult { Success = false }); var success = Task.FromResult(new SecretValidationResult { Success = true }); if (parsedSecret.Type != Constants.ParsedSecretTypes.SharedSecret) { Logger.Debug(string.Format("Parsed secret should not be of type: {0}", parsedSecret.Type ?? "null")); return fail; } var sharedSecret = parsedSecret.Credential as string; if (parsedSecret.Id.IsMissing() || sharedSecret.IsMissing()) { throw new ArgumentException("Id or Credential is missing."); } foreach (var secret in secrets) { var secretDescription = string.IsNullOrEmpty(secret.Description) ? "no description" : secret.Description; // this validator is only applicable to shared secrets if (secret.Type != Constants.SecretTypes.SharedSecret) { Logger.Debug(string.Format("Skipping secret: {0}, secret is not of type {1}.", secretDescription, Constants.SecretTypes.SharedSecret)); continue; } // use time constant string comparison var isValid = TimeConstantComparer.IsEqual(sharedSecret, secret.Value); if (isValid) { return success; } } Logger.Debug("No matching plain text secret found."); return fail; }
/// <summary> /// Tries to find a JWT client assertion token on the environment that can be used for authentication /// Used for "private_key_jwt" client authentication method as defined in http://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication /// </summary> /// <param name="environment">The environment.</param> /// <returns> /// A parsed secret /// </returns> public async Task<ParsedSecret> ParseAsync(IDictionary<string, object> environment) { Logger.Debug("Start parsing for JWT client assertion in post body"); var context = new OwinContext(environment); var body = await context.ReadRequestFormAsync(); if (body != null) { var clientId = body.Get(Constants.TokenRequest.ClientId); var clientAssertionType = body.Get(Constants.TokenRequest.ClientAssertionType); var clientAssertion = body.Get(Constants.TokenRequest.ClientAssertion); if (clientAssertion.IsPresent() && clientAssertionType == Constants.ClientAssertionTypes.JwtBearer) { if (!clientId.IsPresent()) { // at least some clients (i.e. java com.nimbusds/oauth2-oidc-sdk) do not send client_id, but assume that token is enough (and it actually is) clientId = GetClientIdFromToken(clientAssertion); if (!clientId.IsPresent()) { return null; } } var parsedSecret = new ParsedSecret { Id = clientId, Credential = clientAssertion, Type = Constants.ParsedSecretTypes.JwtBearer }; return parsedSecret; } } Logger.Debug("No JWT client assertion found in post body"); return null; }
/// <summary> /// Validates a secret /// </summary> /// <param name="secrets">The stored secrets.</param> /// <param name="parsedSecret">The received secret.</param> /// <returns> /// A validation result /// </returns> /// <exception cref="System.ArgumentException">id or credential is missing.</exception> public Task<SecretValidationResult> ValidateAsync(IEnumerable<Secret> secrets, ParsedSecret parsedSecret) { var fail = Task.FromResult(new SecretValidationResult { Success = false }); var success = Task.FromResult(new SecretValidationResult { Success = true }); if (parsedSecret.Type == Constants.ParsedSecretTypes.SharedSecret) { var sharedSecret = parsedSecret.Credential as string; if (parsedSecret.Id.IsMissing() || sharedSecret.IsMissing()) { throw new ArgumentException("id or credential is missing."); } foreach (var secret in secrets) { // this validator is only applicable to shared secrets if (secret.Type != Constants.SecretTypes.SharedSecret) { continue; } // check if client secret is still valid if (secret.Expiration.HasExpired()) continue; // use time constant string comparison var isValid = TimeConstantComparer.IsEqual(sharedSecret, secret.Value); if (isValid) { return success; } } } return fail; }
public async Task Invalid_Expired_Token() { var clientId = "certificate_valid"; var client = await _clients.FindClientByIdAsync(clientId); var token = CreateToken(clientId, DateTime.Now.AddHours(-1)); var secret = new ParsedSecret { Id = clientId, Credential = new JwtSecurityTokenHandler().WriteToken(token), Type = Constants.ParsedSecretTypes.JwtBearer }; var result = await _validator.ValidateAsync(client.ClientSecrets, secret); result.Success.Should().BeFalse(); }
public async Task Invalid_Unsigned_Token() { var clientId = "certificate_valid"; var client = await _clients.FindClientByIdAsync(clientId); var token = CreateToken(clientId); token.Header.Remove("alg"); token.Header.Add("alg", "none"); var secret = new ParsedSecret { Id = clientId, Credential = new JwtSecurityTokenHandler().WriteToken(token), Type = Constants.ParsedSecretTypes.JwtBearer }; var result = await _validator.ValidateAsync(client.ClientSecrets, secret); result.Success.Should().BeFalse(); }
public async Task Valid_Certificate_Thumbprint() { var clientId = "certificate_valid"; var client = await _clients.FindClientByIdAsync(clientId); var secret = new ParsedSecret { Id = clientId, Credential = new JwtSecurityTokenHandler().WriteToken(CreateToken(clientId)), Type = Constants.ParsedSecretTypes.JwtBearer }; var result = await _validator.ValidateAsync(client.ClientSecrets, secret); result.Success.Should().BeTrue(); }
public async Task Client_with_no_Secret_Should_Throw() { var clientId = "no_secret_client"; var client = await _clients.FindClientByIdAsync(clientId); var secret = new ParsedSecret { Id = clientId, Type = Constants.ParsedSecretTypes.SharedSecret }; Func<Task> act = () => _validator.ValidateAsync(client.ClientSecrets, secret); act.ShouldThrow<ArgumentException>(); }
public async Task Invalid_Multiple_Secrets() { var clientId = "multiple_secrets_no_protection"; var client = await _clients.FindClientByIdAsync(clientId); var secret = new ParsedSecret { Id = clientId, Credential = "invalid", Type = Constants.ParsedSecretTypes.SharedSecret }; var result = await _validator.ValidateAsync(client.ClientSecrets, secret); result.Success.Should().BeFalse(); }
/// <summary> /// Validates a secret /// </summary> /// <param name="secrets">The stored secrets.</param> /// <param name="parsedSecret">The received secret.</param> /// <returns> /// A validation result /// </returns> /// <exception cref="System.ArgumentNullException">Id or cedential</exception> public Task<SecretValidationResult> ValidateAsync(IEnumerable<Secret> secrets, ParsedSecret parsedSecret) { var fail = Task.FromResult(new SecretValidationResult { Success = false }); var success = Task.FromResult(new SecretValidationResult { Success = true }); if (parsedSecret.Type != Constants.ParsedSecretTypes.SharedSecret) { Logger.Debug(string.Format("Parsed secret should not be of type {0}", parsedSecret.Type ?? "null")); return fail; } var sharedSecret = parsedSecret.Credential as string; if (parsedSecret.Id.IsMissing() || sharedSecret.IsMissing()) { throw new ArgumentException("Id or Credential is missing."); } var secretSha256 = sharedSecret.Sha256(); var secretSha512 = sharedSecret.Sha512(); foreach (var secret in secrets) { var secretDescription = string.IsNullOrEmpty(secret.Description) ? "no description" : secret.Description; // this validator is only applicable to shared secrets if (secret.Type != Constants.SecretTypes.SharedSecret) { Logger.Debug(string.Format("Skipping secret: {0}, secret is not of type {1}.", secretDescription, Constants.SecretTypes.SharedSecret)); continue; } // check if client secret is still valid if (secret.Expiration.HasExpired()) { Logger.Debug(string.Format("Skipping secret: {0}, secret is expired.", secretDescription)); continue; } bool isValid = false; byte[] secretBytes; try { secretBytes = Convert.FromBase64String(secret.Value); } catch (FormatException) { Logger.Error(string.Format("Secret: {0} uses invalid hashing algorithm.", secretDescription)); return fail; } if (secretBytes.Length == 32) { isValid = TimeConstantComparer.IsEqual(secret.Value, secretSha256); } else if (secretBytes.Length == 64) { isValid = TimeConstantComparer.IsEqual(secret.Value, secretSha512); } else { Logger.Error(string.Format("Secret: {0} uses invalid hashing algorithm.", secretDescription)); return fail; } if (isValid) { return success; } } Logger.Debug("No matching hashed secret found."); return fail; }
/// <summary> /// Validates a secret /// </summary> /// <param name="secrets">The stored secrets.</param> /// <param name="parsedSecret">The received secret.</param> /// <returns> /// A validation result /// </returns> /// <exception cref="System.ArgumentNullException">Id or cedential</exception> public Task<SecretValidationResult> ValidateAsync(IEnumerable<Secret> secrets, ParsedSecret parsedSecret) { var fail = Task.FromResult(new SecretValidationResult { Success = false }); var success = Task.FromResult(new SecretValidationResult { Success = true }); if (parsedSecret.Type == Constants.ParsedSecretTypes.SharedSecret) { var sharedSecret = parsedSecret.Credential as string; if (parsedSecret.Id.IsMissing() || sharedSecret.IsMissing()) { throw new ArgumentNullException("Id or cedential"); } var secretSha256 = sharedSecret.Sha256(); var secretSha512 = sharedSecret.Sha512(); foreach (var secret in secrets) { // this validator is only applicable to shared secrets if (secret.Type != Constants.SecretTypes.SharedSecret) { continue; } bool isValid = false; byte[] secretBytes; // check if client secret is still valid if (secret.Expiration.HasExpired()) continue; try { secretBytes = Convert.FromBase64String(secret.Value); } catch (FormatException) { Logger.Error("Secret uses invalid hashing algorithm"); return fail; } if (secretBytes.Length == 32) { isValid = TimeConstantComparer.IsEqual(secret.Value, secretSha256); } else if (secretBytes.Length == 64) { isValid = TimeConstantComparer.IsEqual(secret.Value, secretSha512); } else { Logger.Error("Secret uses invalid hashing algorithm"); return fail; } if (isValid) { return success; } } } return fail; }
public async Task Valid_Certificate_X5c_Only() { var clientId = "certificate_valid"; var client = await _clients.FindClientByIdAsync(clientId); var token = CreateToken(clientId); token.Header.Remove(JwtHeaderParameterNames.X5t); var secret = new ParsedSecret { Id = clientId, Credential = new JwtSecurityTokenHandler().WriteToken(token), Type = Constants.ParsedSecretTypes.JwtBearer }; var result = await _validator.ValidateAsync(client.ClientSecrets, secret); result.Success.Should().BeTrue(); }
public async Task Expired_Secret() { var clientId = "multiple_secrets_hashed"; var client = await _clients.FindClientByIdAsync(clientId); var secret = new ParsedSecret { Id = clientId, Credential = "expired", Type = Constants.ParsedSecretTypes.SharedSecret }; var result = await _validator.ValidateAsync(secret, client.ClientSecrets); result.Success.Should().BeFalse(); }
public async Task Invalid_Subject() { var clientId = "certificate_valid"; var client = await _clients.FindClientByIdAsync(clientId); var token = CreateToken(clientId); token.Payload.Remove(JwtClaimTypes.Subject); token.Payload.Add(JwtClaimTypes.Subject, "invalid"); var secret = new ParsedSecret { Id = clientId, Credential = new JwtSecurityTokenHandler().WriteToken(token), Type = Constants.ParsedSecretTypes.JwtBearer }; var result = await _validator.ValidateAsync(client.ClientSecrets, secret); result.Success.Should().BeFalse(); }
/// <summary> /// Validates a secret /// </summary> /// <param name="secrets">The stored secrets.</param> /// <param name="parsedSecret">The received secret.</param> /// <returns> /// A validation result /// </returns> /// <exception cref="System.ArgumentException">ParsedSecret.Credential is not a JWT token</exception> public Task<SecretValidationResult> ValidateAsync(IEnumerable<Secret> secrets, ParsedSecret parsedSecret) { var fail = Task.FromResult(new SecretValidationResult { Success = false }); var success = Task.FromResult(new SecretValidationResult { Success = true }); if (parsedSecret.Type != Constants.ParsedSecretTypes.JwtBearer) { return fail; } var jwtTokenString = parsedSecret.Credential as string; if (jwtTokenString == null) { throw new ArgumentException("ParsedSecret.Credential is not a string."); } var enumeratedSecrets = secrets.ToList().AsReadOnly(); var trustedKeys = GetTrustedKeys(enumeratedSecrets, jwtTokenString); if (!trustedKeys.Any()) { Logger.Warn("There are no certificates available to validate client assertion."); return fail; } var tokenValidationParameters = new TokenValidationParameters { IssuerSigningKeys = trustedKeys, ValidateIssuerSigningKey = true, ValidIssuer = parsedSecret.Id, ValidateIssuer = true, ValidAudience = audienceUri, ValidateAudience = true, RequireSignedTokens = true, RequireExpirationTime = true }; try { SecurityToken token; var handler = new EmbeddedCertificateJwtSecurityTokenHandler(); handler.ValidateToken(jwtTokenString, tokenValidationParameters, out token); var jwtToken = (JwtSecurityToken)token; if (jwtToken.Subject != jwtToken.Issuer) { Logger.Warn("Both 'sub' and 'iss' in the client assertion token must have a value of client_id."); return fail; } return success; } catch (Exception e) { Logger.Debug("JWT token validation error: " + e.Message); return fail; } }
/// <summary> /// Tries to find a secret on the environment that can be used for authentication /// </summary> /// <param name="environment">The environment.</param> /// <returns> /// A parsed secret /// </returns> public Task<ParsedSecret> ParseAsync(IDictionary<string, object> environment) { Logger.Debug("Start parsing Basic Authentication secret"); var notfound = Task.FromResult<ParsedSecret>(null); var context = new OwinContext(environment); var authorizationHeader = context.Request.Headers.Get("Authorization"); if (authorizationHeader == null) { return notfound; } if (!authorizationHeader.StartsWith("Basic ", StringComparison.OrdinalIgnoreCase)) { return notfound; } var parameter = authorizationHeader.Substring("Basic ".Length); string pair; try { pair = Encoding.UTF8.GetString( Convert.FromBase64String(parameter)); } catch (FormatException) { Logger.Debug("Malformed Basic Authentication credential."); return notfound; } catch (ArgumentException) { Logger.Debug("Malformed Basic Authentication credential."); return notfound; } var ix = pair.IndexOf(':'); if (ix == -1) { Logger.Debug("Malformed Basic Authentication credential."); return notfound; } var clientId = pair.Substring(0, ix); var secret = pair.Substring(ix + 1); if (clientId.IsPresent() && secret.IsPresent()) { var parsedSecret = new ParsedSecret { Id = clientId, Credential = secret, Type = Constants.ParsedSecretTypes.SharedSecret }; return Task.FromResult(parsedSecret); } Logger.Debug("No Basic Authentication secret found"); return notfound; }