I'm using ASP.NET Core to serve an API to an Android client. Android signs in as a Google account and passes a JWT, the ID Token, to API as a bearer token. I have the app working, it does pass the auth checks, but I don't think it's validating the token signature.
Per Google's documents, I can call this url to do it: https://www.googleapis.com/oauth2/v3/tokeninfo?id_token=XYZ123, but I can't find the appropriate hooks on the server side to do it. Also according to the Google docs, I can somehow use the Client Access APIs to do it without calling to the server every time.
My configuration code:
app.UseJwtBearerAuthentication( new JwtBearerOptions() { Authority = "https://accounts.google.com", Audience = "hiddenfromyou.apps.googleusercontent.com", TokenValidationParameters = new TokenValidationParameters() { ValidateAudience = true, ValidIssuer = "accounts.google.com" }, RequireHttpsMetadata = false, AutomaticAuthenticate = true, AutomaticChallenge = false, });
How do I get the JWTBearer middleware to validate the signature? I'm close to giving up on using the MS middleware and rolling my own.
After you receive the ID token by HTTPS POST, you must verify the integrity of the token. To verify that the token is valid, ensure that the following criteria are satisfied: The ID token is properly signed by Google. Use Google's public keys (available in JWK or PEM format) to verify the token's signature.
There are two ways to verify a token: locally or remotely with Okta. The token is signed with a JSON Web Key (JWK) using the RS256 algorithm. To validate the signature, Okta provides your application with a public key that can be used.
To sign in or sign up a user with an ID token, send the token to your app's backend. On the backend, verify the token using either a Google API client library or a general-purpose JWT library. If the user hasn't signed in to your app with this Google Account before, create a new account.
Firebase ID tokens are short lived and last for an hour; the refresh token can be used to retrieve new ID tokens. Refresh tokens expire only when one of the following occurs: The user is deleted. The user is disabled.
There are a couple of different ways in which you can validate the integrity of the ID token on the server side:
iss
one; the main advantage (albeit a small one in my opinion) I see here is that you can minimize the number of requests sent to Google.https://www.googleapis.com/oauth2/v3/tokeninfo?id_token={0}
Here's how the second one could look:
private const string GoogleApiTokenInfoUrl = "https://www.googleapis.com/oauth2/v3/tokeninfo?id_token={0}"; public ProviderUserDetails GetUserDetails(string providerToken) { var httpClient = new MonitoredHttpClient(); var requestUri = new Uri(string.Format(GoogleApiTokenInfoUrl, providerToken)); HttpResponseMessage httpResponseMessage; try { httpResponseMessage = httpClient.GetAsync(requestUri).Result; } catch (Exception ex) { return null; } if (httpResponseMessage.StatusCode != HttpStatusCode.OK) { return null; } var response = httpResponseMessage.Content.ReadAsStringAsync().Result; var googleApiTokenInfo = JsonConvert.DeserializeObject<GoogleApiTokenInfo>(response); if (!SupportedClientsIds.Contains(googleApiTokenInfo.aud)) { Log.WarnFormat("Google API Token Info aud field ({0}) not containing the required client id", googleApiTokenInfo.aud); return null; } return new ProviderUserDetails { Email = googleApiTokenInfo.email, FirstName = googleApiTokenInfo.given_name, LastName = googleApiTokenInfo.family_name, Locale = googleApiTokenInfo.locale, Name = googleApiTokenInfo.name, ProviderUserId = googleApiTokenInfo.sub }; }
GoogleApiTokenInfo class:
public class GoogleApiTokenInfo { /// <summary> /// The Issuer Identifier for the Issuer of the response. Always https://accounts.google.com or accounts.google.com for Google ID tokens. /// </summary> public string iss { get; set; } /// <summary> /// Access token hash. Provides validation that the access token is tied to the identity token. If the ID token is issued with an access token in the server flow, this is always /// included. This can be used as an alternate mechanism to protect against cross-site request forgery attacks, but if you follow Step 1 and Step 3 it is not necessary to verify the /// access token. /// </summary> public string at_hash { get; set; } /// <summary> /// Identifies the audience that this ID token is intended for. It must be one of the OAuth 2.0 client IDs of your application. /// </summary> public string aud { get; set; } /// <summary> /// An identifier for the user, unique among all Google accounts and never reused. A Google account can have multiple emails at different points in time, but the sub value is never /// changed. Use sub within your application as the unique-identifier key for the user. /// </summary> public string sub { get; set; } /// <summary> /// True if the user's e-mail address has been verified; otherwise false. /// </summary> public string email_verified { get; set; } /// <summary> /// The client_id of the authorized presenter. This claim is only needed when the party requesting the ID token is not the same as the audience of the ID token. This may be the /// case at Google for hybrid apps where a web application and Android app have a different client_id but share the same project. /// </summary> public string azp { get; set; } /// <summary> /// The user's email address. This may not be unique and is not suitable for use as a primary key. Provided only if your scope included the string "email". /// </summary> public string email { get; set; } /// <summary> /// The time the ID token was issued, represented in Unix time (integer seconds). /// </summary> public string iat { get; set; } /// <summary> /// The time the ID token expires, represented in Unix time (integer seconds). /// </summary> public string exp { get; set; } /// <summary> /// The user's full name, in a displayable form. Might be provided when: /// The request scope included the string "profile" /// The ID token is returned from a token refresh /// When name claims are present, you can use them to update your app's user records. Note that this claim is never guaranteed to be present. /// </summary> public string name { get; set; } /// <summary> /// The URL of the user's profile picture. Might be provided when: /// The request scope included the string "profile" /// The ID token is returned from a token refresh /// When picture claims are present, you can use them to update your app's user records. Note that this claim is never guaranteed to be present. /// </summary> public string picture { get; set; } public string given_name { get; set; } public string family_name { get; set; } public string locale { get; set; } public string alg { get; set; } public string kid { get; set; } }
According to this github issue, you can now use GoogleJsonWebSignature.ValidateAsync
method to validate a Google-signed JWT. Simply pass the idToken
string to the method.
var validPayload = await GoogleJsonWebSignature.ValidateAsync(idToken); Assert.IsNotNull(validPayload);
If it is not a valid token, it will return null
.
Note that to use this method, you need to install Google.Apis.Auth nuget firsthand.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With