public async Task RetrieveAccessToken_InvalidCertificate_ReturnsUnauthorized() { var options = new OioIdwsAuthorizationServiceOptions { AccessTokenIssuerPath = new PathString("/accesstoken/issue"), AccessTokenRetrievalPath = new PathString("/accesstoken"), IssuerAudiences = () => Task.FromResult(new IssuerAudiences[0]), TrustedWspCertificateThumbprints = new[] { "other cert" }, CertificateValidator = X509CertificateValidator.None //no reason for tests to validate certs }; X509Certificate2 certificate = null; using (var server = TestServerWithClientCertificate.Create(() => certificate, app => { app.UseOioIdwsAuthorizationService(options); })) { server.BaseAddress = new Uri("https://localhost/"); //without presenting certificate var response = await server.HttpClient.GetAsync($"/accesstoken?accesstoken1"); Assert.AreEqual(HttpStatusCode.Unauthorized, response.StatusCode); //with untrusted certificate certificate = CertificateUtil.GetCertificate("d9f10c97aa647727adb64a349bb037c5c23c9a7a"); response = await server.HttpClient.GetAsync($"/accesstoken?accesstoken1"); Assert.AreEqual(HttpStatusCode.Unauthorized, response.StatusCode); } }
async Task PerformValidationTestAsync( X509Certificate2 clientCertificate, IEnumerable <IssuerAudiences> issuerAudiences, Func <OioIdwsAuthorizationServiceOptions, HttpResponseMessage, Task> assert, Func <string> samlToken = null, Func <ILoggerFactory> setLogger = null) { var requestSamlToken = samlToken != null?samlToken() : GetSamlTokenXml(); var options = new OioIdwsAuthorizationServiceOptions { AccessTokenIssuerPath = new PathString("/accesstoken/issue"), AccessTokenRetrievalPath = new PathString("/accesstoken"), IssuerAudiences = () => Task.FromResult(issuerAudiences.ToArray()), CertificateValidator = X509CertificateValidator.None //no reason for tests to require proper certificate validation }; using (var server = TestServerWithClientCertificate.Create(() => clientCertificate, app => { app.UseOioIdwsAuthorizationService(options); if (setLogger != null) { app.SetLoggerFactory(setLogger()); } })) { server.BaseAddress = new Uri("https://localhost/"); var response = await server.HttpClient.PostAsync("/accesstoken/issue", new FormUrlEncodedContent(new[] { new KeyValuePair <string, string>("saml-token", Utils.ToBase64(requestSamlToken)), })); await assert(options, response); } }
public async Task CallWspService_ShortTokenLifeTime_RenegotiatesAccessToken() { var serverEndpoint = "https://digst.oioidws.rest.wsp:10002"; var tokensIssuedCount = 0; using (WebApp.Start(serverEndpoint, app => { app.SetLoggerFactory(new ConsoleLoggerFactory()); var tokenStore = new InMemorySecurityTokenStore(); var tokenStoreWrapper = new Mock <ISecurityTokenStore>(); tokenStoreWrapper .Setup(x => x.RetrieveTokenAsync(It.IsAny <string>())) .Returns((string accessToken) => tokenStore.RetrieveTokenAsync(accessToken)); tokenStoreWrapper .Setup(x => x.StoreTokenAsync(It.IsAny <string>(), It.IsAny <OioIdwsToken>())) .Returns((string accessToken, OioIdwsToken token) => { tokensIssuedCount++; return(tokenStore.StoreTokenAsync(accessToken, token)); }); var authorizationServerOptions = new OioIdwsAuthorizationServiceOptions { AccessTokenIssuerPath = new PathString("/accesstoken/issue"), IssuerAudiences = () => Task.FromResult(new[] { new IssuerAudiences("d9f10c97aa647727adb64a349bb037c5c23c9a7a", "test cert") .Audience(new Uri("https://wsp.oioidws-net.dk")), }), SecurityTokenStore = tokenStoreWrapper.Object, MaxClockSkew = TimeSpan.FromSeconds(10), //a little time skew is needed for trusting STS tokens }; app .UseOioIdwsAuthentication(new OioIdwsAuthenticationOptions()) .UseOioIdwsAuthorizationService(authorizationServerOptions) .Use(async(context, next) => { if (context.Request.User == null) { //we expect the service to REQUIRE authorization context.Response.StatusCode = 401; return; } var identity = (ClaimsIdentity)context.Request.User.Identity; await context.Response.WriteAsync(identity.Claims .Single(x => x.Type == "dk:gov:saml:attribute:CvrNumberIdentifier").Value); }); })) { var settings = new OioIdwsClientSettings { ClientCertificate = CertificateUtil.GetCertificate("0E6DBCC6EFAAFF72E3F3D824E536381B26DEECF5"), AudienceUri = new Uri("https://wsp.oioidws-net.dk"), AccessTokenIssuerEndpoint = new Uri(serverEndpoint + "/accesstoken/issue"), SecurityTokenService = new OioIdwsStsSettings { Certificate = CertificateUtil.GetCertificate("d9f10c97aa647727adb64a349bb037c5c23c9a7a"), EndpointAddress = new Uri("https://SecureTokenService.test-nemlog-in.dk/SecurityTokenService.svc"), TokenLifeTime = TimeSpan.FromMinutes(5) }, DesiredAccessTokenExpiry = TimeSpan.FromSeconds(5), //set a very low token expiry time }; var idwsClient = new OioIdwsClient(settings); { var handler = (OioIdwsRequestHandler)idwsClient.CreateMessageHandler(); var httpClient = new HttpClient(handler) { BaseAddress = new Uri(serverEndpoint) }; //first request, token should be valid var response = await httpClient.GetAsync("/myservice"); Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); var str = await response.Content.ReadAsStringAsync(); Assert.AreEqual("34051178", str); } { var handler = (OioIdwsRequestHandler)idwsClient.CreateMessageHandler(); var httpClient = new HttpClient(handler) { BaseAddress = new Uri(serverEndpoint) }; Thread.Sleep(TimeSpan.FromSeconds(30)); //second request, time has passed, token should be renegotiated var response = await httpClient.GetAsync("/myservice"); Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); var str = await response.Content.ReadAsStringAsync(); Assert.AreEqual("34051178", str); } Assert.AreEqual(2, tokensIssuedCount); } }
public async Task IssueAccessToken_Success_ReturnsCorrectly() { var requestSamlToken = Utils.ToBase64("accesstoken1"); var accessToken = "dummy"; var accessTokenGeneratorMock = new Mock <IKeyGenerator>(); var tokenStoreMock = new Mock <ISecurityTokenStore>(); var tokenValidatorMock = new Mock <ITokenValidator>(); accessTokenGeneratorMock .Setup(x => x.GenerateUniqueKey()) .Returns(accessToken); tokenValidatorMock .Setup(x => x.ValidateTokenAsync(Utils.FromBase64(requestSamlToken), It.IsAny <X509Certificate2>(), It.IsAny <OioIdwsAuthorizationServiceOptions>())) .ReturnsAsync(new TokenValidationResult { Success = true, ClaimsIdentity = new ClaimsIdentity() }); var tokenDataFormatMock = new Mock <ISecureDataFormat <AuthenticationProperties> >(); tokenDataFormatMock .Setup(x => x.Protect(It.IsAny <AuthenticationProperties>())) .Returns(accessToken); var options = new OioIdwsAuthorizationServiceOptions { AccessTokenIssuerPath = new PathString("/accesstoken/issue"), AccessTokenRetrievalPath = new PathString("/accesstoken"), KeyGenerator = accessTokenGeneratorMock.Object, TokenValidator = tokenValidatorMock.Object, IssuerAudiences = () => Task.FromResult(new [] { new IssuerAudiences("thumbprint1", "name"), }), SecurityTokenStore = tokenStoreMock.Object, TokenDataFormat = tokenDataFormatMock.Object, }; using (var server = TestServer.Create(app => { app.UseOioIdwsAuthorizationService(options); })) { server.BaseAddress = new Uri("https://localhost/"); var response = await server.HttpClient.PostAsync("/accesstoken/issue", new FormUrlEncodedContent(new[] { new KeyValuePair <string, string>("saml-token", requestSamlToken), })); Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); Assert.IsNotNull(response.Content.Headers.ContentType); Assert.AreEqual("UTF-8", response.Content.Headers.ContentType.CharSet); Assert.AreEqual("application/json", response.Content.Headers.ContentType.MediaType); var accesssTokenFromResponse = JObject.Parse(await response.Content.ReadAsStringAsync()); Assert.AreEqual(accessToken, accesssTokenFromResponse["access_token"]); Assert.AreEqual("Bearer", accesssTokenFromResponse["token_type"]); Assert.AreEqual((int)options.AccessTokenExpiration.TotalSeconds, accesssTokenFromResponse["expires_in"]); } accessTokenGeneratorMock.Verify(x => x.GenerateUniqueKey(), Times.Once); }
public async Task IssueAccessToken_SetShouldExpireIn_HonorsExpiration() { var requestSamlToken = Utils.ToBase64("samltoken1"); var expiration = 200; var issuedAt = new DateTimeOffset(2016, 1, 1, 12, 00, 0, TimeSpan.Zero); AuthenticationProperties authenticationProperties = null; //set during token protection var clockMock = new Mock <ISystemClock>(); clockMock.SetupGet(x => x.UtcNow).Returns(issuedAt); var tokenStoreMock = new Mock <ISecurityTokenStore>(); var tokenValidatorMock = new Mock <ITokenValidator>(); tokenValidatorMock .Setup(x => x.ValidateTokenAsync(Utils.FromBase64(requestSamlToken), It.IsAny <X509Certificate2>(), It.IsAny <OioIdwsAuthorizationServiceOptions>())) .ReturnsAsync(new TokenValidationResult { ClaimsIdentity = new ClaimsIdentity(), AccessTokenType = AccessTokenType.Bearer, Success = true, }); var tokenDataFormatMock = new Mock <ISecureDataFormat <AuthenticationProperties> >(); tokenDataFormatMock .Setup(x => x.Protect(It.IsAny <AuthenticationProperties>())) .Returns((AuthenticationProperties props) => { authenticationProperties = props; return("tokenvalue"); }); var authorizationOptions = new OioIdwsAuthorizationServiceOptions { AccessTokenIssuerPath = new PathString("/accesstoken/issue"), AccessTokenRetrievalPath = new PathString("/accesstoken"), IssuerAudiences = () => Task.FromResult(new IssuerAudiences[0]), SecurityTokenStore = tokenStoreMock.Object, TokenValidator = tokenValidatorMock.Object, SystemClock = clockMock.Object, TokenDataFormat = tokenDataFormatMock.Object, }; using (var server = TestServer.Create(app => { app.Use <OioIdwsAuthorizationServiceMiddleware>(app, authorizationOptions); })) { server.BaseAddress = new Uri("https://localhost/"); var response = await server.HttpClient.PostAsync("/accesstoken/issue", new FormUrlEncodedContent(new Dictionary <string, string> { { "saml-token", requestSamlToken }, { "should-expire-in", expiration.ToString() }, })); Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); var tokenInfo = JObject.Parse(await response.Content.ReadAsStringAsync()); Assert.AreEqual(expiration, tokenInfo["expires_in"]); var expectedExpiration = issuedAt + TimeSpan.FromSeconds(expiration); Assert.AreEqual(expectedExpiration, authenticationProperties.ExpiresUtc); tokenStoreMock.Verify(v => v.StoreTokenAsync(authenticationProperties.Dictionary["value"], It.Is <OioIdwsToken>(x => x.ExpiresUtc == authenticationProperties.ExpiresUtc.Value))); } }
public async Task RetrieveAccessToken_Success_TokenInformationIsInResponse() { var wspCertificate = CertificateUtil.GetCertificate("d9f10c97aa647727adb64a349bb037c5c23c9a7a"); var accessToken = "dummy"; var oioIdwsTokenKey = "accesstoken1"; var token = new OioIdwsToken { Type = AccessTokenType.Bearer, ExpiresUtc = DateTime.UtcNow.AddHours(1), Claims = new[] { new OioIdwsClaim { Type = "type1", Value = "value1", ValueType = "valuetype1", Issuer = "issuer1", }, new OioIdwsClaim { Type = "type2", Value = "value2", ValueType = "valuetype2", Issuer = "issuer2", }, } }; var tokenStoreMock = new Mock <ISecurityTokenStore>(); tokenStoreMock .Setup(x => x.RetrieveTokenAsync(oioIdwsTokenKey)) .ReturnsAsync(token); var tokenDataFormatMock = new Mock <ISecureDataFormat <AuthenticationProperties> >(); tokenDataFormatMock .Setup(x => x.Unprotect(accessToken)) .Returns(new AuthenticationProperties { ExpiresUtc = DateTimeOffset.UtcNow.AddHours(1), Dictionary = { { "value", oioIdwsTokenKey } } }); var options = new OioIdwsAuthorizationServiceOptions { AccessTokenIssuerPath = new PathString("/accesstoken/issue"), AccessTokenRetrievalPath = new PathString("/accesstoken"), IssuerAudiences = () => Task.FromResult(new IssuerAudiences[0]), SecurityTokenStore = tokenStoreMock.Object, TokenDataFormat = tokenDataFormatMock.Object, TrustedWspCertificateThumbprints = new[] { "d9f10c97aa647727adb64a349bb037c5c23c9a7a" }, CertificateValidator = X509CertificateValidator.None //no reason for tests to validate certs }; using (var server = TestServerWithClientCertificate.Create(() => wspCertificate, app => { app.UseOioIdwsAuthorizationService(options); })) { server.BaseAddress = new Uri("https://localhost/"); var response = await server.HttpClient.GetAsync($"/accesstoken?{accessToken}"); Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); Assert.AreEqual("application/json", response.Content.Headers.ContentType.MediaType); var responseToken = JsonConvert.DeserializeObject <OioIdwsToken>(await response.Content.ReadAsStringAsync()); Assert.AreEqual(token.Type, responseToken.Type); Assert.AreEqual(token.ExpiresUtc, responseToken.ExpiresUtc); Assert.AreEqual(token.Claims.Count, responseToken.Claims.Count); Assert.AreEqual(token.Claims.ElementAt(0).Type, responseToken.Claims.ElementAt(0).Type); Assert.AreEqual(token.Claims.ElementAt(0).Value, responseToken.Claims.ElementAt(0).Value); } }
public async Task RetrieveAccessToken_ExpiredAccessToken_ReturnsUnauthorized() { var wspCertificate = CertificateUtil.GetCertificate("d9f10c97aa647727adb64a349bb037c5c23c9a7a"); var accessToken = "accessToken1"; var oioIdwsTokenKey = "tokenValue1"; var tokenInformation = new OioIdwsToken(); var authProperties = new AuthenticationProperties { Dictionary = { { "value", oioIdwsTokenKey } } }; var tokenDataFormatMock = new Mock <ISecureDataFormat <AuthenticationProperties> >(); tokenDataFormatMock .Setup(x => x.Unprotect(accessToken)) .Returns(() => authProperties); var currentTime = DateTimeOffset.UtcNow; //ensure static time during test var timeMock = new Mock <ISystemClock>(); // ReSharper disable once AccessToModifiedClosure timeMock .SetupGet(x => x.UtcNow) .Returns(() => currentTime); var storeMock = new Mock <ISecurityTokenStore>(); storeMock .Setup(x => x.RetrieveTokenAsync(oioIdwsTokenKey)) .Returns(() => Task.FromResult(tokenInformation)); var options = new OioIdwsAuthorizationServiceOptions { AccessTokenIssuerPath = new PathString("/accesstoken/issue"), AccessTokenRetrievalPath = new PathString("/accesstoken"), IssuerAudiences = () => Task.FromResult(new IssuerAudiences[0]), TrustedWspCertificateThumbprints = new[] { "d9f10c97aa647727adb64a349bb037c5c23c9a7a" }, CertificateValidator = X509CertificateValidator.None, //no reason for tests to validate certs TokenDataFormat = tokenDataFormatMock.Object, SystemClock = timeMock.Object, MaxClockSkew = TimeSpan.FromMinutes(5), SecurityTokenStore = storeMock.Object, }; using (var server = TestServerWithClientCertificate.Create(() => wspCertificate, app => { app.UseOioIdwsAuthorizationService(options); })) { server.BaseAddress = new Uri("https://localhost/"); { //test that token content is checked properly authProperties.ExpiresUtc = currentTime - options.MaxClockSkew.Add(TimeSpan.FromSeconds(1)); var response = await server.HttpClient.GetAsync($"/accesstoken?{accessToken}"); Assert.AreEqual(HttpStatusCode.Unauthorized, response.StatusCode); var json = JObject.Parse(await response.Content.ReadAsStringAsync()); Assert.AreEqual(1, json["expired"].Value <int>()); } { //test that stored token information is checked properly authProperties.ExpiresUtc = currentTime; tokenInformation.ExpiresUtc = currentTime - options.MaxClockSkew.Add(TimeSpan.FromSeconds(1)); var response = await server.HttpClient.GetAsync($"/accesstoken?{accessToken}"); Assert.AreEqual(HttpStatusCode.Unauthorized, response.StatusCode); var json = JObject.Parse(await response.Content.ReadAsStringAsync()); Assert.AreEqual(1, json["expired"].Value <int>()); } } }
public async Task <TokenValidationResult> ValidateTokenAsync(string token, X509Certificate2 clientCertificate, OioIdwsAuthorizationServiceOptions options) { var samlHandler = new Saml2SecurityTokenHandler { Configuration = new SecurityTokenHandlerConfiguration { ServiceTokenResolver = options.ServiceTokenResolver, } }; Saml2SecurityToken securityToken; try { securityToken = ParseToken(token, samlHandler); } catch (Exception ex) { //If it's an CryptographicException the token might have been tampered with, e.g. signature validation failed //If it's an EncryptedTokenDecryptionFailedException the handler could not locate the proper certificate via the specified ServiceTokenResolver //Finally it could be a host of miscellanious errors during parsing which should not happen under normal circumstances. Either way, the exception is passed on for logging return(TokenValidationResult.Error("Token could not be parsed", ex)); } var accessTokenType = DetermineAccessTokenType(securityToken); if (accessTokenType == AccessTokenType.HolderOfKey) { X509RawDataKeyIdentifierClause key; try { key = GetSecurityKeyIdentifierClause(securityToken); } catch (Exception) { return(TokenValidationResult.Error("X509Certificate used as SubjectConfirmationData could not read from the token")); } if (!key.Matches(clientCertificate)) { return(TokenValidationResult.Error("X509Certificate used as SubjectConfirmationData did not match the provided client certificate")); } } else if (accessTokenType == AccessTokenType.Bearer) { //no validation needed } else { return(TokenValidationResult.Error("SubjectConfirmation method was not valid")); } var issuerToken = securityToken.IssuerToken as X509SecurityToken; if (issuerToken == null) { return(TokenValidationResult.Error("IssuerToken is expected to be of type X509SecurityToken")); } var issuerAudience = (await options.IssuerAudiences()).SingleOrDefault(x => x.IssuerThumbprint == issuerToken.Certificate.Thumbprint?.ToLowerInvariant()); if (issuerAudience == null) { return(TokenValidationResult.Error($"Issuer certificate '{issuerToken.Certificate.Thumbprint}' was unknown")); } samlHandler.Configuration.CertificateValidator = options.CertificateValidator; samlHandler.Configuration.CertificateValidationMode = X509CertificateValidationMode.Custom; samlHandler.Configuration.MaxClockSkew = options.MaxClockSkew; samlHandler.Configuration.IssuerNameRegistry = new SpecificIssuerNameRegistry(issuerAudience.IssuerThumbprint, issuerAudience.IssuerFriendlyName); samlHandler.Configuration.AudienceRestriction = new AudienceRestriction(); issuerAudience.ToList().ForEach(a => samlHandler.Configuration.AudienceRestriction.AllowedAudienceUris.Add(a)); ClaimsIdentity identity; try { identity = samlHandler.ValidateToken(securityToken).First(); } catch (AudienceUriValidationFailedException) { return(TokenValidationResult.Error("Audience was not known")); } catch (SecurityTokenExpiredException) { return(TokenValidationResult.Error("The Token is expired")); } catch (SecurityTokenValidationException ex) { //This covers whatever else validation errors might happen, such as token not yet valid and replay attacks return(TokenValidationResult.Error("The Token could not be validated", ex)); } return(new TokenValidationResult { Success = true, AccessTokenType = accessTokenType.Value, ClaimsIdentity = identity, }); }