Reputation: 56
I have a multiplatform app on android and iOS.
At the moment is not possible to have google sign in working for bot iOS app and Android app, right?
On the documentation, it is stated that the iOS app should work if we add the client web application configuration, but this is not true. https://www.mongodb.com/docs/atlas/app-services/authentication/google/#std-label-auth-google-configuration
Is there any workaround for this?
Same issue reported here:
https://github.com/realm/realm-swift/issues/8111
https://github.com/realm/realm-swift/issues/7506
The way that I do the google login is swiftui:
func handleSignInButton() {
GIDSignIn.sharedInstance.signIn(withPresenting: getRootViewController()) {signInResult, error in
guard let result = signInResult else {
// Inspect error
return
}
self.repo.signInWithGoogleLogin(googleUserIDToken: result.user.idToken!.tokenString){user, error in
The method on the shared repo:
suspend fun signInWithGoogleLogin(googleUserIDToken: String): User {
val credentials = Credentials.google(googleUserIDToken, GoogleAuthType.ID_TOKEN)
return appService.login(credentials)
}
And yes, I did add the client id and client secret for the OAuth web on app services.
Upvotes: 1
Views: 504
Reputation: 17793
I ran into the same issue, but with my React Native app, using https://github.com/FormidableLabs/react-native-app-auth/.
The root cause is that the Realm's Google OAuth setup requires you to use an OAuth 2.0 Client ID 'Web application' credential in the Google Cloud Console. However, when you try to use this Client ID configuration in the client app, the Google login UI will complain with this error: "Authorisation Error" 400: Custom scheme URIs are not allowed to 'WEB' client type
. And that is because for safety reasons, Google doesn't allow you to redirect to custom URL schemes to facilitate the redirect back to an app, because it's purely meant for (as it says) web applications, redirecting to web-accessible URLs only.
Instead, you should use the recommended specific 'iOS' or 'Android' OAuth 2.0 Client ID credentials in the Google Cloud Console, like you already did. Those OAuth credentials are designed to work with the app-based scheme redirect URLs. Additional security via the Proof Key for Code Exchange (PKCE) protocol is recommended, which these Client IDs support and encourage.
Now the problem is that different Client IDs present themselves as a different "audience", and this is included as the "aud" claim on the JWT token. Unfortunately, it's not possible to ask the Google OAuth 2.0 server to include additional audiences, so you get the one assigned for your specific Client ID.
And due to the audience check, tokens issued with different Client IDs are therefore not interchangeable. So when we try to use the idToken
from the iOS/Android specific Client IDs with the Web application one for Realm, it rejects the login with the error 47 - invalid id token: ‘aud’ must be a string containing the client_id
.
This really should be fixed from MongoDB Atlas' side, it should simply allow to define multiple Client ID configurations for the single Google OAuth provider.
As a workaround you can use the Custom JWT authentication provider. It supports multiple audiences (Client IDs), and verifies the public keys with a JKWS URI.
See: https://www.mongodb.com/docs/atlas/app-services/authentication/custom-jwt/
https://www.googleapis.com/oauth2/v3/certs
, as defined in Google's OpenID Connect (OIDC) Discovery file.openid
, email
and profile
scope when authenticating, you can extract the following fields from the JWT:name
- Will be assigned to the display name in the Admin UI.email
- Will be assigned to the email address in the Admin UI.email_verified
(boolean)given_name
family_name
picture
locale
e.g. "en"
for English.Any of these audiences
.As another workaround, you could do this yourself though, by writing your own token validation logic with the Function provider. Because really all the Google OAuth provider does, is validate the idToken, and check that it is signed by a trusted party.
Refer to: https://www.mongodb.com/docs/atlas/app-services/authentication/custom-function/
Big disclaimer: I am not a security expert, test and review the below code and use at your own risk!
jsonwebtoken
@ 8.5.1
- Used for verifying tokens.jwks-rsa
@ 1.12.3
- For fetching public keys. NOTE: This old version has a vulnerable jose
dependency! 🙈 But newer versions cannot be used due to limitations of the Node environment set by MongoDB.lodash
- For checking isString/isObject.yallist
- Peer dependency of jwks-rsa
, without it it doesn't work.exports({idToken: "YOUR_JWT_HERE"})
Note the comment at the beginning of the function to easily try some of the error logic.idToken
parameter with the OpenID JWT token you received from your existing iOS/Android Client IDs.const allowed = {
issuers: {
'https://accounts.google.com': {
userIdPrefix: 'google:',
/** @see https://github.com/auth0/node-jsonwebtoken?tab=readme-ov-file#jwtverifytoken-secretorpublickey-options-callback */
validationOptions: {
maxAge: '5s',
// TODO: Update with your Client IDs below.
audience: [
'010101010101-a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1.apps.googleusercontent.com',
'020202020202-b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2.apps.googleusercontent.com',
],
}
}
}
};
/**
* This function will be run when a user logs in with this provider.
*
* The return object must contain a string id, this string id will be used to login with an existing
* or create a new user. This is NOT the App Services user id, but it is the id used to identify which user has
* been created or logged in with.
*
* If an error is thrown within the function the login will fail.
*/
exports = async (loginPayload) => {
// loginPayload.idToken = utils.jwt.encode('HS256', {iss: 'https://evil.example.com'}, 'secret'); // Uncomment for testing an invalid token.
// First, parse the issuer from the provided OpenID idToken JWT.
const idToken = loginPayload.idToken;
const isString = require('lodash/isString');
if(!isString(idToken)) throw new Error('Missing required "idToken" argument.');
const jwtLib = require('jsonwebtoken'); // NOTE: Package version must be < 9 (e.g. 8.5.1) to support Node v10.
const unsafeDecodedJwt = jwtLib.decode(idToken);
const isObject = require('lodash/isObject');
if(!isObject(unsafeDecodedJwt)) throw new Error('Unable to decode "idToken" as a JWT.');
const issuerClaim = unsafeDecodedJwt.iss;
if(!isString(issuerClaim)) throw new Error('JWT missing claim "iss".');
// Verify that the issuer is allowed.
const issuerConfig = allowed.issuers[issuerClaim];
if(!isObject(issuerConfig)) throw new Error('Disallowed issuer in JWT.');
// Retrieve public signing keys via the JWKS URI.
const response = await context.http.get({ url: issuerClaim + '/.well-known/openid-configuration' });
if(response.statusCode !== 200) throw new Error('Failed retrieving OpenID configuration from well-known URL.');
const openIdConfig = JSON.parse(response.body.text());
if(!isObject(openIdConfig)) throw new Error('Unable to parse OpenID configuration from well-known URL.');
if(openIdConfig.issuer !== issuerClaim) throw new Error('Issuer mismatch in OpenID conigurationfrom from well-known URL.');
const jwksUri = openIdConfig.jwks_uri;
if(!isString(jwksUri)) throw new Error('"jwks_uri" missing in OpenID conigurationfrom from well-known URL.');
// NOTE: Package 'yallist' must be installed as a peer dependency.
const jwksRsa = require('jwks-rsa'); // NOTE: Package version must be < 2 (e.g. 1.12.3) to support Node v10 without BigInt, see: https://www.mongodb.com/docs/atlas/app-services/functions/javascript-support/#
const jwksClient = jwksRsa({jwksUri});
const getKey = (header, callback) => jwksClient.getSigningKey(header.kid, (err, key) => err ? callback(err) : callback(null, key.publicKey || key.rsaPublicKey));
// Parse and verify the OpenID idToken JWT.
return new Promise((resolve, reject) => {
jwtLib.verify(idToken, getKey, issuerConfig.validationOptions, function(err, decodedJwt) {
if(err) {
reject(new Error(`Invalid OpenID JWT: ${err}`));
return;
}
const sub = decodedJwt.sub;
if(!isString(sub)) throw new Error('OpenID JWT mising "sub" claim.');
resolve({id: `${issuerConfig.userIdPrefix || ''}${sub}`});
});
});
};
Some improvements for the above code could be caching the request to fetch the OpenID config, and caching the kid
lookup to get the public keys. From my own testing, the processing can be up to 400ms per function request, likely due to network latency from hosting in a Singapore based (AWS) data center.
Also, this login code doesn't prevent replay attacks, where an attacker was able to access the idToken somehow and retries the login. However, it does try to reduce the chance by setting a maximum time-window for this attack with an allowed age of the token of 5 seconds. You could work around this by having a separate function producing nonce values, persisted to a collection. Then the app can request one before the login, and pass it in via the auth flow. Then the above code can check that the JWT contains the "nonce" claim AND verify it exists in your collection AND then immediately deletes it from the collection. Login retries with the same idToken should then fail because the nonce is not retrievable in the store.
Upvotes: 0