Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix: Add id_token grant flow #189

Merged
merged 11 commits into from
Nov 1, 2021
Merged

Fix: Add id_token grant flow #189

merged 11 commits into from
Nov 1, 2021

Conversation

kangmingtay
Copy link
Member

@kangmingtay kangmingtay commented Aug 24, 2021

What kind of change does this PR introduce?

Allows a user to send an id_token & a nonce via POST /token?grant_type=id_token to signup / signin a user

Context

Some native mobile frameworks (such as Swift UI Authentication Services, Expo AuthSession) support their own existing OAuth2.0 authorization flow. The most common (and recommended) OAuth flow for native apps is the Authorization Code flow with proof of key for code exchange.

What is the current behavior?

Currently, mobile developers that make use of their respective native frameworks for auth have difficulties integrating with gotrue.

What is the new behavior?

Mobile developers can use their own native frameworks for auth and send the id_token to gotrue via POST /token?grant_type=id_token endpoint to register a user in the auth schema. This partially addresses #140 and #198.

Example

POST /token?grant_type=id_token
{
  "id_token": "id_token_with_nonce",
  "nonce": "unhashed_nonce_in_id_token",
  "provider": "apple | google | azure | facebook"
}

Limitations

Since the id_token is specific to the OpenID Connect (OIDC) standard, this new endpoint will only be able to support OIDC compliant providers for the meantime.

How the overall flow would look like

image

@kangmingtay kangmingtay requested review from awalias and inian August 24, 2021 10:06
@kangmingtay kangmingtay self-assigned this Aug 24, 2021
@kangmingtay kangmingtay added work-in-progress enhancement New feature or request labels Aug 24, 2021
@kangmingtay kangmingtay force-pushed the feat/verify_id_token branch from ac6f095 to 6d513f0 Compare August 24, 2021 10:12
@kangmingtay kangmingtay force-pushed the feat/verify_id_token branch 2 times, most recently from dda0450 to d781e7c Compare August 30, 2021 07:42
@kangmingtay kangmingtay requested a review from darora August 30, 2021 07:46
Copy link
Member

@awalias awalias left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just a couple of small things, but overall looks ace

api/token.go Outdated Show resolved Hide resolved
api/token.go Outdated Show resolved Hide resolved
@awalias awalias self-requested a review September 9, 2021 16:05
Copy link
Member

@awalias awalias left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

perfect! will leave you to merge it when you ready

@He1nr1chK
Copy link

Hi @kangmingtay, any update on when this will get merged? Super keen for this feature.

@libovness
Copy link

Same here!

@kangmingtay
Copy link
Member Author

Hey @He1nr1chK , @libovness, we are working on a refactor here that will fix a couple of issues with the "Sign-up with an external OAuth provider" flow so this PR is unlikely to be merged yet. Some of the issues we aim to fix before introducing more sign-up flows (like this one) would be the following:

  1. Sign-ups via an external provider relies on email as the primary identifier / OAuth fails when no email is linked to provider account Oauth fails when no email is linked to account  #214
  2. Updating the email associated with the provider results in loss of access to user on gotrue
  3. Only the first provider's metadata is stored, and subsequent sign-ins with other providers are not

In term of timeline, I'm hoping to get the refactoring work done by this week and make the necessary changes to this PR next week.

Thanks for understanding!

@libovness
Copy link

hi @kangmingtay checking in to see if the PR will happen this week?

@vbylen
Copy link

vbylen commented Sep 28, 2021

Looks very exciting, crossing fingers for a quick merge 🤞

@thecoolwinter
Copy link

Any updates/timeline? I'm excited to integrate these changes with the iOS libraries so we can support Sign in With Apple

@libovness
Copy link

gm

@kangmingtay
Copy link
Member Author

kangmingtay commented Oct 6, 2021

Hey @libovness, @thecoolwinter, @10000multiplier, @He1nr1chK, @k0shk0sh, apologies for the delay, i'm still testing out this feature on my end and have some questions regarding the auth flow for ios / android:

  • For android, is the android package name globally unique? (similar to the bundle identifier for ios apps)
  • For external oauth authentication (sign-in with google) for android, I'm assuming the auth flow depends on the native mobile sdk / framework being used, but will one always get back an id_token at the end?
  • If so, will the audience field in the id_token be set to the android package name during the auth flow?

Just to provide some context: Ideally, we want a developer to be able to create a mobile and web app for the same product that uses the same auth server (gotrue) for user authentication. Currently, this implementation doesn't allow for one to build a mobile and web app for the same project using supabase. Based on what i've investigated so far, for sign-in with apple, there's a slight difference between the IOS and web flow.

  • For web, the developer has to create a "Service ID" on apple developer portal.
  • For IOS, after the developer creates the xcode project, the "App ID" will automatically be generated by xcode on the apple developer portal.
  • On gotrue, the "Apple Client ID" maps to the identifier for the "Service ID" created. This identifier is different from the "Bundle ID" in the "App ID" created for IOS.

The issue arises when validating the id_token sent via this endpoint because we need to check for the following:

  1. Validate jwt signature of the id_token
  2. Validate nonce sent
  3. Validate audience (aka "Apple Client ID" for sign-in with apple)

Since the "Apple Client ID" maps to the identifier for the "Service ID" created, only sign-in with apple via web will be allowed.

Once again, apologies for the delay in getting this out. Was pretty busy with the refactoring work in the last month and just got back from a trip this week 😅 Thanks so much for the patience so far!

@thecoolwinter
Copy link

thecoolwinter commented Oct 6, 2021

Since the "Apple Client ID" maps to the identifier for the "Service ID" created, only sign-in with apple via web will be allowed.

I don't think that will be a problem. For iOS the App ID and Service ID are separate things, but are linkable.

To set up SWIA I would go and make a new app and set its Bundle ID, then create a Service ID. When creating that Service ID I'll actually link the Service ID to the bundle ID that I just created. So the Service ID can be used for the same mobile and web app.

Actually, the Service ID can be linked to any number of Bundle IDs or web IDs by grouping their identifiers so that won't be a problem at all.

Thank you for all your hard work 🙏 this is awesome!

@kangmingtay
Copy link
Member Author

@thecoolwinter Hmm based on my understanding, the "Service ID" only needs to be created when you want to have an accompanying website to your IOS app. By default, the IOS app will use the bundle ID for SIWA.

Enabling SIWA on the "Service ID" then grants permissions for users to SIWA on the website. This uses the identifier in the "Service ID" which is different from the bundle ID.

Taken from the Service ID Configuration page:
image

@He1nr1chK
Copy link

He1nr1chK commented Oct 7, 2021

  • For android, is the android package name globally unique? (similar to the bundle identifier for ios apps)
  • For external oauth authentication (sign-in with google) for android, I'm assuming the auth flow depends on the native mobile sdk / framework being used, but will one always get back an id_token at the end?
  • If so, will the audience field in the id_token be set to the android package name during the auth flow?
  1. Yes, the package name is a globally unique identifier across the Playstore and third party Android stores.
  2. I came over from using Firebase so can only speak to my experience regarding that but as far as I can remember using the native auth flow on mobile (React Native & Expo) Google would return both an access_token and id_token and both would be used to authenticate. Facebook would only return an access token which granted access to the Facebook Graph API and Apple would return an id_token and you would have to generate a nonce which would both in turn be used to authenticate through Firebase. Don't know if that helps though. Haven't used it in 6 months so this might have changed in the mean time.

Don't apologise for doing us all a huge favour @kangmingtay, I would have looked into doing this myself but honestly was too lazy. Keep up the good work. Looking forward to using this in the near future. Let me know if this reply helps at all, if not I will gladly assist in any other way I can.

@He1nr1chK
Copy link

  • For android, is the android package name globally unique? (similar to the bundle identifier for ios apps)
  • For external oauth authentication (sign-in with google) for android, I'm assuming the auth flow depends on the native mobile sdk / framework being used, but will one always get back an id_token at the end?
  • If so, will the audience field in the id_token be set to the android package name during the auth flow?
  1. Yes, the package name is a globally unique identifier across the Playstore and third party Android stores.
  2. I came over from using Firebase so can only speak to my experience regarding that but as far as I can remember using the native auth flow on mobile (React Native & Expo) Google would return both an access_token and id_token and both would be used to authenticate. Facebook would only return an access token which granted access to the Facebook Graph API and Apple would return an id_token and you would have to generate a nonce which would both in turn be used to authenticate through Firebase. Don't know if that helps though. Haven't used it in 6 months so this might have changed in the mean time.

Don't apologise for doing us all a huge favour @kangmingtay, I would have looked into doing this myself but honestly was too lazy. Keep up the good work. Looking forward to using this in the near future. Let me know if this reply helps at all, if not I will gladly assist in any other way I can.

Here is an abridged example of the Apple sign in version

const csrf = Math.random()
  .toString(36)
  .substring(2, 15);

const nonce = Math.random()
  .toString(36)
  .substring(2, 10);

const hashedNonce = await Crypto.digestStringAsync(
  Crypto.CryptoDigestAlgorithm.SHA256,
  nonce
);

const appleCredential = await AppleAuthentication.signInAsync({
  requestedScopes: [
    AppleAuthentication.AppleAuthenticationScope.FULL_NAME,
    AppleAuthentication.AppleAuthenticationScope.EMAIL
  ],
  state: csrf,
  nonce: hashedNonce
});

const {
  identityToken,
  email,
  state,
  givenName,
  familyName
} = appleCredential;
const provider = new firebase.auth.OAuthProvider('apple.com');
const credential = provider.credential({
  idToken: identityToken,
  rawNonce: nonce // nonce value from above
});

Google would look like this:

const { type, user } = await GoogleSignIn.signInAsync();

if (type === 'success') {
  this.onGoogleSignIn(user);
  return user;
} else {
  return { cancelled: true };
}

onGoogleSignIn = googleUser => {
  // We need to register an Observer on Firebase Auth to make sure auth is initialized.
  var unsubscribe = firebase.auth().onAuthStateChanged(async firebaseUser => {
    unsubscribe();
    // Check if we are already signed-in Firebase with the correct user.
    if (!isGoogleUserEqual(googleUser, firebaseUser)) {
      // Build Firebase credential with the Google ID token.
      var credential = firebase.auth.GoogleAuthProvider.credential(
        googleUser.auth.idToken,
        googleUser.auth.accessToken
      );

      // Sign in with credential from the Google user.
      const signInResult = await signInWithFirebase(credential);
      if (signInResult.success) {
        const { result } = signInResult;
        const uid = result.user.uid;

        if (result.additionalUserInfo.isNewUser) {
          //Is a new user
        } else {
          //Is a returning user
        }
      } else {
        // An error occurred while signing user in
        const error = signInResult.error;
        console.log('Firebase Auth Error: ', error);
      }
    } else {
      console.log('User already signed into Firebase.');
    }
  });
};

isGoogleUserEqual = (googleUser, firebaseUser) => {
  if (firebaseUser) {
    var providerData = firebaseUser.providerData;
    for (var i = 0; i < providerData.length; i++) {
      if (
        providerData[i].providerId ===
          firebase.auth.GoogleAuthProvider.PROVIDER_ID &&
        providerData[i].uid === googleUser.user.uid
      ) {
        // We don't need to reauth the Firebase connection.
        return true;
      }
    }
  }
  return false;
};

And finally Facebook:

await Facebook.initializeAsync({ appId });
const {
  type,
  token,
  expires,
  permissions,
  declinedPermissions
} = await Facebook.logInWithReadPermissionsAsync({
  permissions: ['public_profile, email']
});
if (type === 'success') {
  // Get the user's name using Facebook's Graph API
  const credential = firebase.auth.FacebookAuthProvider.credential(token);
  const signInResult = await signInWithFirebase(credential);
  if (signInResult.success) {
    const { result } = signInResult;
    const uid = result.user.uid;

    if (result.additionalUserInfo.isNewUser) {
      //Is a new user
    } else {
      //Is a returning user
    }
  } else {
    // An error occurred while signing user in
    const error = signInResult.error;
    console.log('Firebase Facebook Auth Error: ', error);
  }
} else {
  // type === 'cancel'
  // User cancelled Facebook sign in
}

Sorry for the long reply @kangmingtay, hopefully it makes your life a bit easier. P.S. don't scrutinise the code too harshly left out some try catch's etc. for brevity's sake.

@He1nr1chK
Copy link

Hi @kangmingtay, any updates?

@kangmingtay
Copy link
Member Author

Hi everyone, it's been a long wait and i'm happy to announce that we'll be merging this feature into gotrue, and releasing it to all supabase projects in the upcoming week. Do note that this endpoint is still in alpha and thus susceptible to changes. Thanks to everyone for the patience and valuable insights in terms of helping me understand the issues the mobile community has been facing so far!

@kangmingtay kangmingtay changed the title [WIP] Fix: Add id_token grant flow Fix: Add id_token grant flow Nov 1, 2021
@kangmingtay kangmingtay merged commit 5c29e94 into master Nov 1, 2021
@kangmingtay kangmingtay deleted the feat/verify_id_token branch November 1, 2021 09:22
@github-actions
Copy link
Contributor

github-actions bot commented Nov 2, 2021

🎉 This PR is included in version 2.2.2 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

@himeshp
Copy link

himeshp commented Nov 6, 2021

hi, @kangmingtay does this require updating the supabase dart library to access the end point?

@bdlukaa
Copy link

bdlukaa commented Nov 6, 2021

@dshukertjr

@dshukertjr
Copy link
Member

@bdlukaa Thanks for the heads up! This is exciting!

@himeshp Yup, this will require us to update the supabase_dart library! Looks like this will be the next big update in supabase_dart!

@HarryET HarryET mentioned this pull request Nov 30, 2021
uxodb pushed a commit to uxodb/auth that referenced this pull request Nov 13, 2024
* chore: add go-oidc package

* fix: add id token grant flow

* fix: add check for valid aud

* chore: improve error msg for query error

* chore: check if provider is enabled

* fix: add ios bundle id config

* fix: update id_token grant flow to match on provider_id
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

9 participants