in sources/Google.Solutions.Apis/Auth/Gaia/GaiaOidcClient.cs [177:283]
protected override async Task<IOidcSession> AuthorizeWithBrowserAsync(
OidcOfflineCredential? offlineCredential,
ICodeReceiver codeReceiver,
CancellationToken cancellationToken)
{
Precondition.Expect(offlineCredential == null ||
offlineCredential.Issuer == OidcIssuer.Gaia,
"Offline credential must be issued by Gaia");
codeReceiver.ExpectNotNull(nameof(codeReceiver));
var initializer = new GaiaCodeFlow.Initializer(
this.endpoint,
this.deviceEnrollment,
this.userAgent)
{
ClientSecrets = this.Registration.ToClientSecrets()
};
if (offlineCredential?.IdToken != null &&
UnverifiedGaiaJsonWebToken.TryDecode(offlineCredential.IdToken, out var offlineIdToken) &&
offlineIdToken != null &&
!string.IsNullOrEmpty(offlineIdToken.Payload.Email))
{
//
// We still have an ID token with an email address, so we can perform
// a "minimal flow":
//
// - use existing email as login hint (to skip account chooser)
// - don't request the email scope again so that consent unbundling
// doesn't apply
//
// NB. The last point is important and the entire point why we're storing
// the ID token: Consent unbundling (i.e., the behavior of the OAuth consent
// screen where it shows unchecked checkboxes for all scopes) only applies
// when we request two or more scopes. By only requesting a single scope,
// we can sidestep consent unbundling, thereby improving UX.
//
initializer.LoginHint = offlineIdToken.Payload.Email;
initializer.Scopes = new[] { Scopes.Cloud };
}
else
{
initializer.Scopes = new[] { Scopes.Cloud, Scopes.Email };
}
try
{
var flow = CreateFlow(initializer);
var app = new AuthorizationCodeInstalledApp(flow, codeReceiver);
var apiCredential = await
app.AuthorizeAsync(null, cancellationToken)
.ConfigureAwait(true);
//
// Verify that all requested scopes have been granted.
//
var grantedScopes = apiCredential.Token.Scope?.Split(' ');
if (initializer.Scopes.Any(
requestedScope => !grantedScopes.Contains(requestedScope)))
{
throw new OAuthScopeNotGrantedException(
"Authorization failed because you have denied access to a " +
"required resource. Sign in again and make sure " +
"to grant access to all requested resources.");
}
try
{
//
// N.B. Do not dispose the flow if the sign-in succeeds as the
// credential object must hold on to it.
//
return CreateSessionAndRegisterTerminateEvent(
flow,
offlineCredential,
apiCredential.Token);
}
catch
{
flow.Dispose();
throw;
}
}
catch (TokenResponseException e) when (
e.Error?.ErrorUri != null &&
e.Error.ErrorUri.StartsWith("https://accounts.google.com/info/servicerestricted"))
{
if (this.deviceEnrollment.State == DeviceEnrollmentState.Enrolled)
{
throw new AuthorizationFailedException(
"Authorization failed because your computer's device certificate is " +
"is invalid or unrecognized. Use the Endpoint Verification extension " +
"to verify that your computer is enrolled and try again.\n\n" +
e.Error.ErrorDescription);
}
else
{
throw new AuthorizationFailedException(
"Authorization failed because a context-aware access requirement " +
"was not met.\n\n" + e.Error.ErrorDescription);
}
}
}