Search code examples
google-cloud-platformfirebase-authenticationgoogle-signingoogle-workspacegoogle-identity

How to add custom claims in JWT/idToken obtained from google workspace login


I have a simple "Sign in with Google" app where only internal users of google workspace can sign in. e.g.

<html>
  <body>
    <script src="https://accounts.google.com/gsi/client" async defer></script>
    <div id="g_id_onload"
      data-client_id="CLIENT_ID"
      data-context="signin"
      data-ux_mode="popup"
      data-callback="handleCredentialResponse">
    </div>

    <div class="g_id_signin"
      data-type="standard"
      data-shape="rectangular"
      data-theme="outline"
      data-text="signin_with"
      data-size="large"
      data-logo_alignment="left">
    </div>
    <script>
      function handleCredentialResponse(response) {
        console.log(response.credential)
        const decodedJwt = decodeJwt(response.credential);
        console.log(decodedJwt)

        console.log("ID: " + decodedJwt.sub);
        console.log('Full Name: ' + decodedJwt.name);
        console.log('Given Name: ' + decodedJwt.given_name);
        console.log('Family Name: ' + decodedJwt.family_name);
        console.log("Image URL: " + decodedJwt.picture);
        console.log("Email: " + decodedJwt.email);
      }

      function decodeJwt(token) {
        const base64Url = token.split('.')[1];
        const base64 = base64Url.replace('-', '+').replace('_', '/');
        return JSON.parse(window.atob(base64));
      }

    </script>
  </body>
</html>

Requirement is to get custom claims in JWT/idToken (based on custom user attributes or groups to support RBAC on API gateway). What's the best way to achieve that?

This is what I have tried.

  1. I followed this adding-custom-roles-to-jwt-on-login-with-google-identity-platform article and linked beforeSignIn hook to a cloud function that is returning a hard-coded custom claim
    const gcipCloudFunctions = require('gcip-cloud-functions');
    const authClient = new gcipCloudFunctions.Auth();
    exports.beforeSignIn = authClient.functions().beforeSignInHandler((user, context) => {
      console.log({
        user,
        context
      });
      return {
        customClaims: {
          "roleCustomClaim": "SomeRole"
        }
      };
    });

This approach doesn't work with gsi (google identity services) client snippet that I shared above. Cloud function isn't executed (verified using logs) Why is that?

  1. I added "Google" Identity Provider in Identity Platform's provider tab and configured it with same internal web client used in above snippet.

<html>
  <body>
    <script src="https://www.gstatic.com/firebasejs/8.0/firebase.js"></script>
    <script>
      var config = // config copied from identity platform's "Application setup details"
      firebase.initializeApp(config);
      const provider = new firebase.auth.GoogleAuthProvider();
      const auth = firebase.auth();
      firebase.auth()
        .signInWithPopup(provider)
        .then((result) => {
          console.log(result)
          /** @type {firebase.auth.OAuthCredential} */
          var credential = result.credential;
          const idToken = credential.idToken;
          console.log(idToken)
          const decodedJwt = decodeJwt(idToken);
          console.log(decodedJwt)
          // This gives you a Google Access Token. You can use it to access the Google API.
          var token = credential.accessToken;
          // The signed-in user info.
          var user = result.user;
          console.log(user)
          // ...
        }).catch((error) => {
          // Handle Errors here.
          var errorCode = error.code;
          var errorMessage = error.message;
          // The email of the user's account used.
          var email = error.email;
          // The firebase.auth.AuthCredential type that was used.
          var credential = error.credential;
          // ...
        });

      function decodeJwt(token) {
        const base64Url = token.split('.')[1];
        const base64 = base64Url.replace('-', '+').replace('_', '/');
        return JSON.parse(window.atob(base64));
      }
      
    </script>
  </body>
</html>

Now cloud function is being executed with 200 status on user login but I am still not getting custom claim in jwt/idToken. Any idea what I am doing wrong?

I can see a warning "Note: Blocking functions are only available for use with Identity Platform. They are not supported with Firebase Authentication." on Customizing the authentication flow using blocking functions but this is confusing since Docs and even Application setup details of Google Identity platform points to firebase and is using firebase SDK.


Solution

  • token in the result of result.user.getIdTokenResult() method contains custom claims added in authClient.functions().beforeSignInHandler handler in GCP cloud function. I was checking result.credential.idToken which doesn't contain any custom custom claims.

    Another much better method to pass custom claims is to use Google workspace SAML app integration with Google Identity Platform. This way can pass any Google Directory attribute (built-in or custom) to Identity platform without creating any cloud function (Although this approach still supports extension via cloud functions)

    SAM custom attribute mapping

    Example jwt with custom claims (where you can see stackoverflowRole in sign_in_attributes provided by our SAML provider which is google workspace):

    {
      "iss": "https://securetoken.google.com/some-project-123456",
      "aud": "some-project-123456",
      "auth_time": 1657706938,
      "user_id": "someuserid",
      "sub": "someuserid",
      "iat": 1657706938,
      "exp": 1657710538,
      "email": "[email protected]",
      "email_verified": true,
      "firebase": {
        "identities": {
          "saml.customdomain.com": [
            "[email protected]"
          ],
          "email": [
            "[email protected]"
          ]
        },
        "sign_in_provider": "saml.customdomain.com",
        "sign_in_attributes": {
          "firstName": "Abdul",
          "lastName": "Rauf",
          "groups": "custom-superuser",
          "stackoverflowRole": "superuser"
        }
      }
    }
    

    Reference: