wwauth/Google.Solutions.WWAuth/Adapters/StsAdapter.cs (181 lines of code) (raw):

// // Copyright 2022 Google LLC // // Licensed to the Apache Software Foundation (ASF) under one // or more contributor license agreements. See the NOTICE file // distributed with this work for additional information // regarding copyright ownership. The ASF licenses this file // to you under the Apache License, Version 2.0 (the // "License"); you may not use this file except in compliance // with the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. // using Google.Apis.Auth.OAuth2; using Google.Apis.Auth.OAuth2.Responses; using Google.Apis.CloudSecurityToken.v1; using Google.Apis.CloudSecurityToken.v1.Data; using Google.Apis.Logging; using Google.Apis.Util; using Google.Solutions.WWAuth.Data; using Google.Solutions.WWAuth.Util; using Newtonsoft.Json; using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; namespace Google.Solutions.WWAuth.Adapters { /// <summary> /// Adapter for the STS API. /// </summary> public interface IStsAdapter { /// <summary> /// Exchange external token against STS token. /// </summary> Task<TokenResponse> ExchangeTokenAsync( ISubjectToken externalToken, IList<string> scopes, CancellationToken cancellationToken); /// <summary> /// Introspect access token. /// </summary> Task<ISubjectToken> IntrospectTokenAsync( string accessToken, CancellationToken cancellationToken); } internal class StsAdapter : IStsAdapter { private readonly ILogger logger; // // NB. Client secrets are optional and only required for introspection. // private readonly ClientSecrets clientSecrets; internal const string DefaultTokenUrl = "https://sts.googleapis.com/v1/token"; public string Audience { get; } private CloudSecurityTokenService CreateService() { return new CloudSecurityTokenService( new Apis.Services.BaseClientService.Initializer() { ApplicationName = UserAgent.Default.ToString() }); } public StsAdapter( string audience, ClientSecrets clientSecrets, ILogger logger) { this.clientSecrets = clientSecrets; this.Audience = audience.ThrowIfNull(nameof(audience)); this.logger = logger.ThrowIfNull(nameof(logger)); } public async Task<TokenResponse> ExchangeTokenAsync( ISubjectToken externalToken, IList<string> scopes, CancellationToken cancellationToken) { try { this.logger.Info( "Exchanging token for audience '{0}'", this.Audience); using (var service = CreateService()) { var response = await service.V1 .Token( new GoogleIdentityStsV1ExchangeTokenRequest() { Audience = this.Audience, GrantType = "urn:ietf:params:oauth:grant-type:token-exchange", RequestedTokenType = "urn:ietf:params:oauth:token-type:access_token", Scope = string.Join(" ", scopes), SubjectTokenType = externalToken.Type.GetDescription(), SubjectToken = externalToken.Value, }) .WithCredentials< Google.Apis.CloudSecurityToken.v1.V1Resource.TokenRequest, GoogleIdentityStsV1ExchangeTokenResponse>(this.clientSecrets) .ExecuteAsync(cancellationToken) .ConfigureAwait(false); this.logger.Info("Successfully exchanged token"); return new TokenResponse() { AccessToken = response.AccessToken, ExpiresInSeconds = response.ExpiresIn, TokenType = response.TokenType }; } } catch (GoogleApiException e) { // // Try to convert the exception. // var tokenException = TokenExchangeException.FromApiException(e); this.logger.Error(tokenException, "{0}", tokenException.Message); throw tokenException; } } public async Task<ISubjectToken> IntrospectTokenAsync( string accessToken, CancellationToken cancellationToken) { try { using (var service = CreateService()) { var response = await service.V1 .Introspect( new GoogleIdentityStsV1IntrospectTokenRequest() { Token = accessToken, TokenTypeHint = "urn:ietf:params:oauth:token-type:access_token" }) .WithCredentials< Google.Apis.CloudSecurityToken.v1.V1Resource.IntrospectRequest, GoogleIdentityStsV1IntrospectTokenResponse>(this.clientSecrets) .ExecuteAsync(cancellationToken) .ConfigureAwait(false); this.logger.Info("Successfully introspected token"); return new TokenInfo(accessToken, response); } } catch (GoogleApiException e) { // // Try to convert the exception. // var tokenException = TokenExchangeException.FromApiException(e); this.logger.Error(tokenException, "{0}", tokenException.Message); throw tokenException; } } //--------------------------------------------------------------------- // Token info. //--------------------------------------------------------------------- private class TokenInfo : ISubjectToken { public SubjectTokenType Type => SubjectTokenType.IdToken; public string Value { get; } public string Issuer { get; } public DateTimeOffset? Expiry { get; } public IDictionary<string, object> Attributes { get; } public string Audience { get; } public bool IsEncrypted => false; public TokenInfo( string value, GoogleIdentityStsV1IntrospectTokenResponse response) { this.Value = value; this.Issuer = response.Iss; if (response.Exp.HasValue) { this.Expiry = DateTimeOffset.FromUnixTimeSeconds(response.Exp.Value); } this.Attributes = new Dictionary<string, object> { { "iat", response.Iat }, { "scope", response.Scope }, { "sub", response.Sub }, { "username", response.Username }, { "active", response.Active } }; } } } public class TokenExchangeException : Exception { public TokenExchangeException(string message) : base(message) { } public TokenExchangeException( string message, Exception inner) : base(message, inner) { } public static TokenExchangeException FromApiException( GoogleApiException e) { // // The STS returns errors in OAuth format and not in the // "standard" format. This trips up the client library // (b/197825518). // // Try to extract and parse the raw error response. // if (e.Error?.ErrorResponseContent is var jsonError && jsonError != null) { var error = JsonConvert.DeserializeObject<TokenErrorResponse>(jsonError); return new TokenExchangeException(error.ErrorDescription); } else { return new TokenExchangeException("Failed to exchange token", e); } } } }