diff --git a/Src/Support/Google.Apis.Auth.Tests/OAuth2/GoogleCredentialTests.cs b/Src/Support/Google.Apis.Auth.Tests/OAuth2/GoogleCredentialTests.cs index 97cc76e089a..61dcd84e450 100644 --- a/Src/Support/Google.Apis.Auth.Tests/OAuth2/GoogleCredentialTests.cs +++ b/Src/Support/Google.Apis.Auth.Tests/OAuth2/GoogleCredentialTests.cs @@ -846,6 +846,148 @@ public async Task UniverseDomain_DifferentCustomInRequestAndCredential() Assert.Null(request.Headers.Authorization?.Parameter); } + [Theory] + [MemberData(nameof(CredentialFactory_Success_Data))] + public void FromFile_WithType(string json, string credentialType, Type expectedType) + { + var tempFile = Path.GetTempFileName(); + try + { + File.WriteAllText(tempFile, json); + var credential = CredentialFactory.FromFile(tempFile, credentialType); + Assert.IsType(expectedType, credential.UnderlyingCredential); + } + finally + { + File.Delete(tempFile); + } + } + + [Theory] + [MemberData(nameof(CredentialFactory_Failure_Data))] + public void FromFile_WithType_Failure(string json, string credentialType, string expectedMessage) + { + var tempFile = Path.GetTempFileName(); + try + { + File.WriteAllText(tempFile, json); + var ex = Assert.Throws(() => CredentialFactory.FromFile(tempFile, credentialType)); + Assert.Equal(expectedMessage, ex.Message); + } + finally + { + File.Delete(tempFile); + } + } + + [Theory] + [MemberData(nameof(CredentialFactory_Success_Data))] + public void FromJson_WithType(string json, string credentialType, Type expectedType) + { + var credential = CredentialFactory.FromJson(json, credentialType); + Assert.IsType(expectedType, credential.UnderlyingCredential); + } + + [Theory] + [MemberData(nameof(CredentialFactory_Failure_Data))] + public void FromJson_WithType_Failure(string json, string credentialType, string expectedMessage) + { + var ex = Assert.Throws(() => CredentialFactory.FromJson(json, credentialType)); + Assert.Equal(expectedMessage, ex.Message); + } + + [Theory] + [MemberData(nameof(CredentialFactory_Success_Data))] + public async Task FromFileAsync_WithType(string json, string credentialType, Type expectedType) + { + var tempFile = Path.GetTempFileName(); + try + { + File.WriteAllText(tempFile, json); + var credential = await CredentialFactory.FromFileAsync(tempFile, credentialType, CancellationToken.None); + Assert.IsType(expectedType, credential.UnderlyingCredential); + } + finally + { + File.Delete(tempFile); + } + } + + [Theory] + [MemberData(nameof(CredentialFactory_Failure_Data))] + public async Task FromFileAsync_WithType_Failure(string json, string credentialType, string expectedMessage) + { + var tempFile = Path.GetTempFileName(); + try + { + File.WriteAllText(tempFile, json); + var ex = await Assert.ThrowsAsync(() => CredentialFactory.FromFileAsync(tempFile, credentialType, CancellationToken.None)); + Assert.Equal(expectedMessage, ex.Message); + } + finally + { + File.Delete(tempFile); + } + } + + public static TheoryData CredentialFactory_Success_Data() => + new TheoryData + { + { FakeUserCredentialFileContents, JsonCredentialParameters.AuthorizedUserCredentialType, typeof(UserCredential) }, + { FakeServiceAccountCredentialFileContents, JsonCredentialParameters.ServiceAccountCredentialType, typeof(ServiceAccountCredential) } + }; + + public static TheoryData CredentialFactory_Failure_Data() => + new TheoryData + { + { FakeUserCredentialFileContents, JsonCredentialParameters.ServiceAccountCredentialType, "The credential type authorized_user is not compatible with the requested type service_account" }, + { FakeUserCredentialFileContents, "invalid_type", "The credential type authorized_user is not compatible with the requested type invalid_type" }, + { "invalid_json", "any_type", "Error deserializing JSON credential data." } + }; + + [Fact] + public void FromStream_WrapsDeserializationException_WithBadJson() + { + string malformedUserCredentialFileContents = FakeUserCredentialFileContents.Replace("}",""); + using Stream stream = new MemoryStream(Encoding.UTF8.GetBytes(malformedUserCredentialFileContents)); + var ex = Assert.Throws(() => CredentialFactory.FromStream(stream)); + Assert.Equal("Error deserializing JSON credential data.", ex.Message); + Assert.IsType(ex.InnerException); + } + + [Fact] + public void FromStream_WrapsDeserializationException() + { + var stream = new MockStream(() => throw new Exception("Underlying exception")); + var ex = Assert.Throws(() => CredentialFactory.FromStream(stream)); + Assert.Equal("Error deserializing JSON credential data.", ex.Message); + Assert.IsType(ex.InnerException); + Assert.Equal("Underlying exception", ex.InnerException.Message); + } + + private class MockStream : Stream + { + private readonly Action _onRead; + + public MockStream(Action onRead) => _onRead = onRead; + + public override bool CanRead => true; + public override bool CanSeek => false; + public override bool CanWrite => false; + public override long Length => throw new NotSupportedException(); + public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); } + + public override void Flush() => throw new NotSupportedException(); + public override int Read(byte[] buffer, int offset, int count) + { + _onRead(); + return 0; + } + public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); + public override void SetLength(long value) => throw new NotSupportedException(); + public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); + } + /// /// Fake implementation of which only supports fetching the /// clock and the access method. diff --git a/Src/Support/Google.Apis.Auth/OAuth2/CredentialFactory.cs b/Src/Support/Google.Apis.Auth/OAuth2/CredentialFactory.cs index e8008862f05..dfcebd5d324 100644 --- a/Src/Support/Google.Apis.Auth/OAuth2/CredentialFactory.cs +++ b/Src/Support/Google.Apis.Auth/OAuth2/CredentialFactory.cs @@ -16,7 +16,6 @@ limitations under the License. using System; using System.IO; -using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -32,6 +31,11 @@ namespace Google.Apis.Auth.OAuth2; /// public static class CredentialFactory { + /// + /// The error message for JSON deserialization failures. + /// + private const string JsonDeserializationErrorMessage = "Error deserializing JSON credential data."; + /// /// Creates a credential of the specified type from a file that contains JSON credential data. /// @@ -45,6 +49,19 @@ public static async Task FromFileAsync(string credentialPath, Cancellation return await FromStreamAsync(fileStream, cancellationToken).ConfigureAwait(false); } + /// + /// Creates a credential of the specified type from a file that contains JSON credential data. + /// + /// The path to the credential file. + /// The type of credential to be loaded. Valid strings can be found in . + /// The cancellation token to cancel the operation. + /// The created credential. + public static async Task FromFileAsync(string credentialPath, string credentialType, CancellationToken cancellationToken) + { + using FileStream fileStream = File.OpenRead(credentialPath); + return await FromStreamAsync(fileStream, credentialType, cancellationToken).ConfigureAwait(false); + } + /// /// Creates a credential of the specified type from a file that contains JSON credential data. /// @@ -57,6 +74,18 @@ public static T FromFile(string credentialPath) return FromStream(fileStream); } + /// + /// Creates a credential of the specified type from a file that contains JSON credential data. + /// + /// The path to the credential file. + /// The type of credential to be loaded. Valid strings can be found in . + /// The created credential. + public static GoogleCredential FromFile(string credentialPath, string credentialType) + { + using FileStream fileStream = File.OpenRead(credentialPath); + return FromStream(fileStream, credentialType); + } + /// /// Creates a credential of the specified type from a stream that contains JSON credential data. /// @@ -66,14 +95,37 @@ public static T FromFile(string credentialPath) /// A task that will be completed with the created credential. public static async Task FromStreamAsync(Stream stream, CancellationToken cancellationToken) { + JsonCredentialParameters jsonCredentialParameters; try { - return FromJsonParameters(await NewtonsoftJsonSerializer.Instance.DeserializeAsync(stream, cancellationToken).ConfigureAwait(false)); + jsonCredentialParameters = await NewtonsoftJsonSerializer.Instance.DeserializeAsync(stream, cancellationToken).ConfigureAwait(false); } catch (Exception e) { - throw new InvalidOperationException("Error deserializing JSON credential data.", e); + throw new InvalidOperationException(JsonDeserializationErrorMessage, e); } + return FromJsonParameters(jsonCredentialParameters); + } + + /// + /// Creates a credential of the specified type from a stream that contains JSON credential data. + /// + /// The stream that contains the JSON credential data. + /// The type of credential to be loaded. Valid strings can be found in . + /// The cancellation token to cancel the operation. + /// The created credential. + private static async Task FromStreamAsync(Stream stream, string credentialType, CancellationToken cancellationToken) + { + JsonCredentialParameters jsonCredentialParameters; + try + { + jsonCredentialParameters = await NewtonsoftJsonSerializer.Instance.DeserializeAsync(stream, cancellationToken).ConfigureAwait(false); + } + catch (Exception e) + { + throw new InvalidOperationException(JsonDeserializationErrorMessage, e); + } + return FromJsonParameters(jsonCredentialParameters, credentialType); } /// @@ -84,14 +136,36 @@ public static async Task FromStreamAsync(Stream stream, CancellationToken /// The created credential. public static T FromStream(Stream stream) { + JsonCredentialParameters jsonCredentialParameters; try { - return FromJsonParameters(NewtonsoftJsonSerializer.Instance.Deserialize(stream)); + jsonCredentialParameters = NewtonsoftJsonSerializer.Instance.Deserialize(stream); } catch (Exception e) { - throw new InvalidOperationException("Error deserializing JSON credential data.", e); + throw new InvalidOperationException(JsonDeserializationErrorMessage, e); } + return FromJsonParameters(jsonCredentialParameters); + } + + /// + /// Creates a credential of the specified type from a stream that contains JSON credential data. + /// + /// The stream that contains the JSON credential data. + /// The type of credential to be loaded. Valid strings can be found in . + /// The created credential. + private static GoogleCredential FromStream(Stream stream, string credentialType) + { + JsonCredentialParameters jsonCredentialParameters; + try + { + jsonCredentialParameters = NewtonsoftJsonSerializer.Instance.Deserialize(stream); + } + catch (Exception e) + { + throw new InvalidOperationException(JsonDeserializationErrorMessage, e); + } + return FromJsonParameters(jsonCredentialParameters, credentialType); } /// @@ -102,14 +176,36 @@ public static T FromStream(Stream stream) /// The created credential. public static T FromJson(string json) { + JsonCredentialParameters jsonCredentialParameters; try { - return FromJsonParameters(NewtonsoftJsonSerializer.Instance.Deserialize(json)); + jsonCredentialParameters = NewtonsoftJsonSerializer.Instance.Deserialize(json); } catch (Exception e) { - throw new InvalidOperationException("Error deserializing JSON credential data.", e); + throw new InvalidOperationException(JsonDeserializationErrorMessage, e); } + return FromJsonParameters(jsonCredentialParameters); + } + + /// + /// Creates a credential of the specified type from a string that contains JSON credential data. + /// + /// The string that contains the JSON credential data. + /// The type of credential to be loaded. Valid strings can be found in . + /// The created credential. + public static GoogleCredential FromJson(string json, string credentialType) + { + JsonCredentialParameters jsonCredentialParameters; + try + { + jsonCredentialParameters = NewtonsoftJsonSerializer.Instance.Deserialize(json); + } + catch (Exception e) + { + throw new InvalidOperationException(JsonDeserializationErrorMessage, e); + } + return FromJsonParameters(jsonCredentialParameters, credentialType); } /// @@ -125,7 +221,8 @@ public static T FromJson(string json) /// internal static T FromJsonParameters(JsonCredentialParameters credentialParameters) { - if (CreateCredential(credentialParameters, typeof(T)) is T credentialAsT) + credentialParameters.ThrowIfNull(nameof(credentialParameters)); + if (FromJsonParameters(credentialParameters, typeof(T)) is T credentialAsT) { return credentialAsT; } @@ -133,19 +230,44 @@ internal static T FromJsonParameters(JsonCredentialParameters credentialParam throw new InvalidOperationException( $"Found incompatible credential types, '{credentialParameters.Type}' and '{typeof(T).FullName}, even though a check" + " should have already taken place. We should never reach here, there's a bug in the code."); + } - static IGoogleCredential CreateCredential(JsonCredentialParameters credentialParameters, Type targetCredentialType) => - credentialParameters.ThrowIfNull(nameof(credentialParameters)).Type switch - { - JsonCredentialParameters.AuthorizedUserCredentialType => CreateUserCredentialFromParameters(credentialParameters, targetCredentialType), - JsonCredentialParameters.ServiceAccountCredentialType => CreateServiceAccountCredentialFromParameters(credentialParameters, targetCredentialType), - JsonCredentialParameters.ExternalAccountCredentialType => CreateExternalCredentialFromParameters(credentialParameters, targetCredentialType), - JsonCredentialParameters.ImpersonatedServiceAccountCredentialType => CreateImpersonatedServiceAccountCredentialFromParameters(credentialParameters, targetCredentialType), - JsonCredentialParameters.ExternalAccountAuthorizedUserCredentialType => CreateExternalAccountAuthorizedUserCredentialFromParameters(credentialParameters, targetCredentialType), - _ => throw new InvalidOperationException($"Error creating credential from JSON or JSON parameters. Unrecognized credential type {credentialParameters.Type}."), - }; + /// + /// Creates a credential of the specified type from JSON credential parameters. + /// + /// The JSON credential parameters. + /// The type of credential to be loaded. Valid strings can be found in . + /// The created credential. + /// + /// Thrown if the is unrecognized, + /// or if the credential data is incompatible with the requested type. + /// + private static GoogleCredential FromJsonParameters(JsonCredentialParameters credentialParameters, string credentialType) + { + credentialParameters.ThrowIfNull(nameof(credentialParameters)); + if (credentialParameters.Type != credentialType) + { + throw new InvalidOperationException($"The credential type {credentialParameters.Type} is not compatible with the requested type {credentialType}"); + } + + // Type checking has already occurred so target type may be set to the generic IGoogleCredential which all credentials implement. + Type targetType = typeof(IGoogleCredential); + var rawCredentialType = FromJsonParameters(credentialParameters, targetType); + + return rawCredentialType.ToGoogleCredential(); } + private static IGoogleCredential FromJsonParameters(JsonCredentialParameters credentialParameters, Type targetCredentialType) => + credentialParameters.ThrowIfNull(nameof(credentialParameters)).Type switch + { + JsonCredentialParameters.AuthorizedUserCredentialType => CreateUserCredentialFromParameters(credentialParameters, targetCredentialType), + JsonCredentialParameters.ServiceAccountCredentialType => CreateServiceAccountCredentialFromParameters(credentialParameters, targetCredentialType), + JsonCredentialParameters.ExternalAccountCredentialType => CreateExternalCredentialFromParameters(credentialParameters, targetCredentialType), + JsonCredentialParameters.ImpersonatedServiceAccountCredentialType => CreateImpersonatedServiceAccountCredentialFromParameters(credentialParameters, targetCredentialType), + JsonCredentialParameters.ExternalAccountAuthorizedUserCredentialType => CreateExternalAccountAuthorizedUserCredentialFromParameters(credentialParameters, targetCredentialType), + _ => throw new InvalidOperationException($"Error creating credential from JSON or JSON parameters. Unrecognized credential type {credentialParameters.Type}.") + }; + /// /// Validates actual type can be cast to target type. ///