public async Task <Token> GetTokenAsync(OAuthOptions configuration, CancellationToken cancellationToken) { Token token = _tokenCache.GetToken(configuration); if (token != null && !token.IsAccessTokenExpired) { return(token); } // TODO: use refresh token if available token = await _client.GetTokenAsync(configuration, cancellationToken); _tokenCache.SaveToken(configuration, token); return(token); }
/// <summary> /// Gets the XML RequestSecurityTokenResponse from the ADFS Secure Token Service (STS). /// </summary> /// <param name="relyingParty"></param> /// <param name="username"></param> /// <param name="password"></param> /// <param name="stsUrl"></param> /// <param name="cached"></param> /// <returns></returns> public async Task <string> GetStsSamlTokenAsync(string relyingParty, string username, string password, string stsUrl, bool cached = true) { string securityTokenResponse; SamlTokenParameters tokenParameters = new SamlTokenParameters(relyingParty, username, stsUrl); if (cached) { _logger.LogDebug("Checking for cached token"); securityTokenResponse = _tokenCache.GetToken(tokenParameters); if (securityTokenResponse != null) { _logger.LogTrace("Returning cached token"); return(securityTokenResponse); } _logger.LogTrace("Cached token not found"); } // Makes a request that conforms with the WS-Trust standard to // the Security Token Service to get a SAML security token back // generate the WS-Trust security token request SOAP message string samlSoapRequest = CreateSamlSoapRequest(relyingParty, username, password, stsUrl); using var content = new StringContent(samlSoapRequest, Encoding.UTF8, "application/soap+xml"); // tried this, it didn't help the slow requests, tokens are cached, so this isn't called on every request //using var handler = new SocketsHttpHandler { MaxConnectionsPerServer = 100 }; //using var client = new HttpClient(handler); using var client = new HttpClient(); var responseMessage = await client.PostAsync(stsUrl, content); // A valid response needs to be a SOAP element, Content Type = application/soap+xml // Invalid parameters can still return 200 but will be an HTML document var responseContent = await responseMessage.Content.ReadAsStringAsync(); if (TryParseXml(responseContent, out XmlDocument soapResponse)) { // <a:Action s:mustUnderstand="1">http://schemas.xmlsoap.org/ws/2005/02/trust/RSTR/Issue</a:Action> // <a:Action s:mustUnderstand="1">http://www.w3.org/2005/08/addressing/soap/fault</a:Action> string action = SelectSoapAction(soapResponse); // The response could also be a SOAP Fault if (!responseMessage.IsSuccessStatusCode) { if (action == "http://www.w3.org/2005/08/addressing/soap/fault") { /* * <s:Body> * <s:Fault> * <s:Code> * <s:Value>s:Sender</s:Value> * <s:Subcode> * <s:Value>a:DestinationUnreachable</s:Value> * </s:Subcode> * </s:Code> * <s:Reason> * <s:Text xml:lang="en-US">The message with To 'https://ststest.gov.bc.ca/adfs/services/trust/2005/UsernameMixed2' cannot be processed at the receiver, due to an AddressFilter mismatch at the EndpointDispatcher. Check that the sender and receiver's EndpointAddresses agree.</s:Text> * </s:Reason> * </s:Fault> * </s:Body> * */ XmlNamespaceManager namespaceManager = GetXmlNamespaceManager(soapResponse); var code = GetNodeInnerText(soapResponse, namespaceManager, "/s:Envelope/s:Body/s:Fault/s:Code/s:Value"); var subcode = GetNodeInnerText(soapResponse, namespaceManager, "/s:Envelope/s:Body/s:Fault/s:Code/s:Subcode"); var reason = GetNodeInnerText(soapResponse, namespaceManager, "/s:Envelope/s:Body/s:Fault/s:Reason/s:Text"); _logger.LogWarning("Request for security token resulted in SOAP Fault and was not successful. STSUrl={StsUrl}, RelyingParty={RelyingParty}, Fault={@Fault}", stsUrl, relyingParty, new { Code = code, Subcode = subcode, Reason = reason }); throw new SamlAuthenticationException($"Request to {stsUrl} return HTTP Status {responseMessage.StatusCode}", code, subcode, reason); } else { _logger.LogWarning("Request for security token was not successful and was not a SOAP Fault. Returned {HttpStatusCode}, STS Url is {StsUrl} and Relying Party {RelyingParty}", responseMessage.StatusCode, stsUrl, relyingParty); } // TODO: change to better custom exception, read any possible content and log throw new SamlAuthenticationException($"Request to {stsUrl} return HTTP Status {responseMessage.StatusCode}", string.Empty, string.Empty, string.Empty); } else if (action == "http://schemas.xmlsoap.org/ws/2005/02/trust/RSTR/Issue") { securityTokenResponse = ExtractSoapBody(soapResponse); // we don't need the lifetime if we are not caching if (cached) { var lifetime = ExtractLifetimeTimestamps(soapResponse); _logger.LogInformation("Security token has {@Lifetime} and will be cached until it expires", lifetime); _tokenCache.SaveToken(tokenParameters, securityTokenResponse, lifetime.Expires); } return(securityTokenResponse); } else { // TODO: throw custom exception related to SOAP fault throw new SamlAuthenticationException($"Unexpected SOAP Action : {action}"); } } else { // response could be a HTML error page // TODO: throw custom exception related to ADFS error page throw new SamlAuthenticationException("Invalid SOAP Response. Content is not valid XML"); } }