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

api implementation for OpenID Connect users #207

Merged
merged 41 commits into from
Feb 7, 2022
Merged

api implementation for OpenID Connect users #207

merged 41 commits into from
Feb 7, 2022

Conversation

koba-ninkigumi
Copy link
Contributor

@koba-ninkigumi koba-ninkigumi commented Jan 14, 2022

What kind of change does this PR introduce?

This is the api implementation for OpenID Connect users.

const { user, session, error } = await supabase.auth.signIn({
  oidc:{
      id_token: 'your idtoken',
      nonce: 'random value',
      provider: 'google'
  }
})

This pull request solves the following issue.

This pull request is made possible by the following pull request.

What is the current behavior?

none.

What is the new behavior?

Numerous issues will be resolved.

Additional context

supabase/supabase#5181

You should check the OpenID Connect specification for details. https://openid.net/developers/specs/

Sign In With Google
https://developers.google.com/identity/gsi/web/guides/overview

Handle credential responses with JavaScript functions
https://developers.google.com/identity/gsi/web/guides/handle-credential-responses-js-functions

↑ You can find instructions on how to get the token here. (It is included in the response)

Sign In With Google HTML API reference
https://developers.google.com/identity/gsi/web/reference/html-reference

Sign In With Google JavaScript API reference
https://developers.google.com/identity/gsi/web/reference/js-reference

↑ Here is the description of the nonce that @dthyresson was requesting.

This is sample code.

<script src="https://cdn.jsdelivr.net/npm/@supabase/supabase-js/dist/umd/supabase.min.js"></script>
<script src="https://accounts.google.com/gsi/client" async defer></script>
<div id="g_id_onload"
     data-client_id="YOUR_GOOGLE_CLIENT_ID"
     data-context="signin"
     data-ux_mode="popup"
     data-callback="handleCredentialResponse"
     data-nonce="random value"
     data-auto_prompt="false">
</div>
<div class="g_id_signin"
     data-type="standard"
     data-shape="rectangular"
     data-theme="outline"
     data-text="$ {button.text}"
     data-size="large"
     data-logo_alignment="left">
</div>
<script>
  function handleCredentialResponse(response) {

    console.log(response)

    ;(async ()=>{
      const { user, session, error } = await supabase.auth.signIn({
        oidc:{
            id_token: response.credential,
            nonce: 'random value',
            provider: 'google'
        }
      })
    })()
  }
</script>

This is the api implementation for OpenID Connect users.

  async signInWithIDToken(
    id_token: string,
    nonce: string,
    provider: string
  )

cf.
- #169
- supabase/auth#189
@dthyresson
Copy link
Contributor

Hi @koba-ninkigumi and thanks much for this PR.

Might you also be able to write up unit tests for a few scenarios?

  • success
  • failures

Given this flow :

and the GoTrue rules in https://github.com/supabase/gotrue/blob/8ba49df7c7bbcc87c9d5b211a32cb5aa17db231d/api/token.go#L316

  • missing required params like "id_token, nonce and provider required
  • what if the nonce does't match
  • missing sub claim in id_token
  • scenario when signups are disabled

This way we can have some tests in place to cover cases developers may encounter when using.

Thanks.

@dthyresson dthyresson self-requested a review January 14, 2022 17:19
Copy link
Contributor

@dthyresson dthyresson left a comment

Choose a reason for hiding this comment

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

Hi, I left some notes and a request to add several tests.

Thanks again for the PR.

@koba-ninkigumi
Copy link
Contributor Author

#207 (comment)

  • scenario when signups are disabled

Enabling OpenID Connect means that you are accepting a huge group of users from OpenID Connect providers (Google, Facebook, etc.) as already signed up.

That's exactly what "OpenID" means.

The idea of disabling signup on the supabase side is incompatible with OpenID Connect.

@koba-ninkigumi
Copy link
Contributor Author

#207 (comment)

  • missing required params like "id_token, nonce and provider required
  • what if the nonce does't match
  • missing sub claim in id_token

If you or I know how to pretend to be an OpenID Connect provider and return id_token, it means that the OpenID Connect specification is broken.

And since we can't get the id_token, we can't write a test.

Please see the following test for reference.

https://github.com/supabase/gotrue-js/blob/master/test/GoTrueClient.test.ts#L182

This is probably the same reason why there are no test cases for oauth providers here.

If you think it is possible to write a test for OpenID Connect, could you please clarify how to do so in more detail?

Renamed the Method.
Added explanation of nonce.
@He1nr1chK
Copy link

Anything I can help with to get this merged? Would love to use this PR asap @koba-ninkigumi @dthyresson

@dthyresson
Copy link
Contributor

Hi @He1nr1chK thanks for offering to help.

I think that these new functions could use:

  • unit tests
  • documentation for how to use them (ie, what one would see and need to know how to use in: https://supabase.com/docs/reference/javascript/auth-signin)
  • And some examples of how to setup and test in a real world case. What are some services I can use and setup (and how) to know that this method works?

@koba-ninkigumi
Copy link
Contributor Author

koba-ninkigumi commented Jan 24, 2022

  • And some examples of how to setup and test in a real world case. What are some services I can use and setup (and how) to know that this method works?

Sign In With Google
https://developers.google.com/identity/gsi/web/guides/overview

Handle credential responses with JavaScript functions
https://developers.google.com/identity/gsi/web/guides/handle-credential-responses-js-functions

↑ You can find instructions on how to get the token here. (It is included in the response)

Sign In With Google HTML API reference
https://developers.google.com/identity/gsi/web/reference/html-reference

Sign In With Google JavaScript API reference
https://developers.google.com/identity/gsi/web/reference/js-reference

↑ Here is the description of the nonce that @dthyresson was requesting.

This is sample code.

<script src="https://cdn.jsdelivr.net/npm/@supabase/supabase-js/dist/umd/supabase.min.js"></script>
<script src="https://accounts.google.com/gsi/client" async defer></script>
<div id="g_id_onload"
     data-client_id="YOUR_GOOGLE_CLIENT_ID"
     data-context="signin"
     data-ux_mode="popup"
     data-callback="handleCredentialResponse"
     data-nonce="random value"
     data-auto_prompt="false">
</div>
<div class="g_id_signin"
     data-type="standard"
     data-shape="rectangular"
     data-theme="outline"
     data-text="$ {button.text}"
     data-size="large"
     data-logo_alignment="left">
</div>
<script>
  function handleCredentialResponse(response) {

    console.log(response)

    ;(async ()=>{
      const { user, session, error } = await supabase.auth.signInWithOpenIDConnect({
        id_token: response.credential,
        nonce: 'random value',
        provider: 'google'
      })
    })()
  }
</script>

@koba-ninkigumi
Copy link
Contributor Author

  • unit tests

As I mentioned above, due to the OpenID Connect specification, it should be impossible to get an idtoken that works properly.
So I don't know how to unit test this method.

If someone thinks it is possible, it would be great if that someone could write the unittest of this method.

@koba-ninkigumi
Copy link
Contributor Author

@dthyresson
I wrote a unittest that expects an error because the idtoken is not valid.

@dthyresson
Copy link
Contributor

dthyresson commented Jan 24, 2022

If someone thinks it is possible, it would be great if that someone could write the unittest of this method.

Could you mock the request and return expected results?

This would ensure that the method signature or other logic doesn't accidentally get changed without some test check.

For example, in a PR the cookie is being mocked to return headers as:

export const mockCookieRequest = ({
   event,
   session,
   method = 'POST',
 }: {
   event?: string
   session?: Session | null
   method?: string
 }) => {
   return {
     method,
     headers: { host: 'localhost:9999' },
     body: { event, session },
   }
 }

 export const mockCookieResponse = () => {
   return {
     getHeader: jest.fn(() => undefined),
     setHeader: jest.fn(),
     status: jest.fn(() => {
       return { json: jest.fn(), end: jest.fn() }
     }),
   }
 }

See: https://github.com/supabase/gotrue-js/pull/198/files

Maybe something similar?

@koba-ninkigumi
Copy link
Contributor Author

Maybe something similar?

Instead of using a cookie as an example, use oauth2 from gotrue.js as an example.
This is because the OpenID Connect specification is an extension of oauth2.

Currently, none of the tests for signIn() in gotrue.js specify a provider.
A Mock that returns an access token would be helpful.

@koba-ninkigumi
Copy link
Contributor Author

I don't know of any other way to get a working access token other than getting the official Google private key.

@koba-ninkigumi
Copy link
Contributor Author

@dthyresson @He1nr1chK

I can't prepare the test that @dthyresson requires. I would like to wait for someone's help.

@He1nr1chK
Copy link

He1nr1chK commented Jan 24, 2022

@dthyresson I have done some research into the possible tests one can write for OIDC and I have to agree with @koba-ninkigumi. It's going to be difficult if not impossible to write a proper test for the function. Is the test absolutely necessary?

@awalias
Copy link
Member

awalias commented Jan 26, 2022

hey @koba-ninkigumi @He1nr1chK this is awesome! Thanks for your hard work.

Is the test absolutely necessary

It would be very useful to have some automated tests to make sure we don't accidentally break the logic later on.

I have done some research into the possible tests one can write for OIDC and I have to agree with @koba-ninkigumi. It's going to be difficult if not impossible to write a proper test for the function

Could you expand on what is the difficult bit? Could we mock the request as @dthyresson suggested?

Again, thanks for all your help on this! 🙏

@koba-ninkigumi
Copy link
Contributor Author

@awalias

OpenID Connect is an authentication. It's a specification for proving "who you are."

Who is the test runner? It is the test runner.

A normal OpenID Connect provider will try to verify that a human actually accessed the site with a browser.
For example, in the case of Google, they ask for confirmation through biometrics or double authentication.

OpenID Connect providers are tasked with rejecting non-human access, such as test runners.

If a test runner is authenticated by OpenID Connect, even if it means a successful test of supabase goture.
It's just a failure on the part of the OpenID Connect provider.

How do you exploit that hole to illegally obtain an idtoken?
And is it a successful test to get the idtoken?
No, it's just a collapse of the system.

@koba-ninkigumi
Copy link
Contributor Author

@awalias

In order to connect to supabase with OpenID Connect, you need to get a legitimate idtoken beforehand.
It's impossible to get that idtoken.

If it were possible to write a test to authenticate the idtoken, it could be done in two ways.

  1. Set up a fake OpenID Connect provider in your test and issue idtokens that work correctly there.

  2. You are a futurist from 1000 years in the future who is familiar with quantum computers and can crack the cipher used in OpenID Connect in less than an hour to get a fake idtoken and put all that logic in the test.

@koba-ninkigumi
Copy link
Contributor Author

@awalias

My suggestion is to release this feature first.
Then, the testing of this feature should be a separate issue.

@koba-ninkigumi
Copy link
Contributor Author

@awalias

I would also add that if you are setting up a fake OpenID Connect Provider in your testing, you will need to provide the necessary information on an externally accessible https server, per the OpenID Connect specification.
Otherwise, it will be considered a fraudulent idtoken based on the oidc specification.

@dthyresson
Copy link
Contributor

Thanks for the PR! I am approving this but I hope that we can address any changes/requests

Yes, again many thanks for your work and patience.

Let's get this in, but maybe test out for a few weeks and if any changes need to be made, then can address -- and then release the docs with those updates.

@github-actions
Copy link
Contributor

github-actions bot commented Feb 9, 2022

🎉 This PR is included in version 1.22.1 🎉

The release is available on:

Your semantic-release bot 📦🚀

@koba-ninkigumi koba-ninkigumi deleted the patch-1 branch February 10, 2022 00:13
@He1nr1chK
Copy link

@koba-ninkigumi @kangmingtay @dthyresson can this function be used yet? I updated my @supabase/supabase-js package to v1.30.3 which exposes this function but when I try to call it, I get TypeError: undefined is not a function

@koba-ninkigumi
Copy link
Contributor Author

@He1nr1chK I'm already using it with Sign In With Google, and google-one-tap is also working fine.

@He1nr1chK
Copy link

@koba-ninkigumi that's fantastic news. How are you invoking the function? Like this:

const { user, session, error } = await supabase.auth.signInWithOpenIDConnect({
      id_token: idToken,
      nonce,
      client_id: Platform.select({
            ios: GOOGLE_IOS_CLIENT_ID,
            android: GOOGLE_ANDROID_CLIENT_ID,
      }),
      issuer: 'https://google.com',
      provider: 'google',
});

@koba-ninkigumi
Copy link
Contributor Author

koba-ninkigumi commented Feb 18, 2022

@He1nr1chK

Please see my first post on this page.

const { user, session, error } = await supabase.auth.signIn({
  oidc:{
      id_token: 'your idtoken',
      nonce: 'random value',
      provider: 'google'
  }
})

@koba-ninkigumi
Copy link
Contributor Author

@He1nr1chK

If you want to specify Google, you should not set issuer and client_id, but write 'google' in provider and set client_id by logging in to the supabase site.

@He1nr1chK
Copy link

Perfect. Thank you @koba-ninkigumi (on the JS implementation) and @kangmingtay (on the Golang implementation) for the amazing work you both did to make this feature a reality. Much appreciated!

@He1nr1chK
Copy link

He1nr1chK commented Feb 22, 2022

Hi @koba-ninkigumi, when using the method with Apple as the provider I get the following error: "oidc: invalid configuration, clientID must be provided or SkipClientIDCheck must be set". In the Supabase console my Apple OAuth is correctly configured. Any idea what might be causing this?

@kangmingtay
Copy link
Member

Hey @He1nr1chK, could you include your request format? Can you try the following request format below and see if it works for you please?

curl -X "POST" "https://PROJECT_REF.supabase.co/auth/v1/token?grant_type=id_token" \
     -H 'apikey: apikey' \
     -H 'Content-Type: application/json; charset=utf-8' \
     -d '{
              "client_id": "client_id",
              "nonce": "nonce",
              "id_token": "id_token",
              "issuer": "https://appleid.apple.com/"
      }'

@He1nr1chK
Copy link

Hi @kangmingtay thanks for getting back to me. This seems to have solved the issue but now I get error {"message": "Database error saving new user", "status": 500} even though I have used this Apple ID to sign in before.

@kangmingtay
Copy link
Member

@He1nr1chK can you please email us at [email protected] with your project reference so we can help to debug further? thanks!

@DanMossa
Copy link

I think this PR doesn't implement linking
supabase/auth#434

@WJimmyCook
Copy link

@He1nr1chK did you ever get Apple authentication working? I've been trying but can't figure it out. Keep getting 500 errors.

@He1nr1chK
Copy link

He1nr1chK commented Apr 5, 2022

Hey @WJimmyCook, yes I did with the following function:

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

  return Crypto.digestStringAsync(Crypto.CryptoDigestAlgorithm.SHA256, nonce)
    .then(hashedNonce =>
      AppleAuthentication.signInAsync({
        requestedScopes: [
          AppleAuthentication.AppleAuthenticationScope.FULL_NAME,
          AppleAuthentication.AppleAuthenticationScope.EMAIL,
        ],
        nonce: hashedNonce,
      }),
    )
    .then(async appleCredential => {
      const { identityToken } = appleCredential;
      const { user, session, error } = await supabase.auth.signIn({
        oidc: {
          id_token: identityToken,
          nonce,
          client_id: '<YOUR_CLIENT_ID>',
          issuer: 'https://appleid.apple.com',
        },
      });
      if (error) {
        console.log(error);
      }
    })
    .catch(error => {
      console.log(error);
    });

I use expo-crypto to hash the nonce before passing the hashedNonce to Apple as is required. Hope this helps!

@WJimmyCook
Copy link

@He1nr1chK You're a legend, bro! Worked like a charm.

@megacherry
Copy link

@He1nr1chK: I hope you could help me also. I always get the error "invalid nonce" - {"message": "invalid nonce", "status": 400}. My provider is google and i tried both OAuth 2.0 Client IDs (the auto created from Google Service and the self created like the documentation mentioned). Both did not work. I also tried the curl-statement from @kangmingtay but it doesn't work also (same error). I don't know where the error come from (i don't think it's from gotrue-js)

My code (simplified) looks like:

import {
  GoogleSignin,
  statusCodes,
} from '@react-native-google-signin/google-signin'
import {v4 as uuid} from 'uuid'

GoogleSignin.configure()
const {idToken} = await GoogleSignin.signIn()
const nonce = uuid()

const {user, error} = await supabase.auth.signIn({
        oidc: {
          id_token: idToken,
          nonce,
          provider: 'google',
        },
})

@WJimmyCook
Copy link

@megacherry

  • Hash the nonce
  • passed the hashed nonce to the google auth request
  • pass the unhashed nonce to supabase
    Here's the general idea
import * as Google from 'expo-auth-session/providers/google';
import * as Crypto from 'expo-crypto';
const nonce = Math.random()
   .toString(36)
   .substring(2, 10);
...
 const [hashedNonce, setHashedNonce] = useState(null);
  useEffect(() => {
       Crypto.digestStringAsync(Crypto.CryptoDigestAlgorithm.SHA256, nonce).then(hNonce => setHashedNonce(hNonce));
   }, []);
...
   const [request, response, promptAsync] = Google.useIdTokenAuthRequest({
       androidClientId,
       extraParams: { nonce: hashedNonce },
   });
...
const { user, session, error } = await supabase.auth.signIn({
       oidc: {
           id_token: response.authentication.idToken,
           nonce,
           provider: 'google',
       },
   });

@megacherry
Copy link

@WJimmyCook

Ok, thank you. I missed the part with the hashed nonce, but i use a bare react-native project with @react-native-google-signin/google-signin and i have no options to pass the hashed nonce to the GoogleSignin.signIn() method.

Do you have any idea?

@WJimmyCook
Copy link

@megacherry not sure what non-expo options there are but I'm running a bare react-native project and I ended up adding expo unimodules (or whatever they call it now) so I can use any expo lib in the bare project.

@wweevv-johndpope
Copy link

wweevv-johndpope commented Nov 15, 2022

I'm hitting a wall with oidc (I'm attempting to use this server side with nodejs) - this seems obsolete?

	const { user, session, error } = await supabase.auth.signIn({
        oidc:{
            id_token: identityToken,
            nonce: nonce,
            provider: 'apple'
        }
      })

ERRORS -> 
 supabase.auth.signIn is not a function

doesn't seem like this got ported to v2 ? am I missing something? there's no mention of this here
https://supabase.com/docs/reference/javascript/auth-signinwithoauth

this is my sample repo - https://github.com/johndpope/Sign-in-with-Apple-for-supabase

@WJimmyCook
Copy link

@wweevv-johndpope I think oidc was removed from v2 but there are plans to add it back based on this comment: supabase/auth#434 (comment)

I'm sticking with v1 until that get fixed.

@ZhenFTW
Copy link

ZhenFTW commented Jan 8, 2023

Hoping the team add it on v2 too, I've been configuring my project on supabase v2 and didn't know that this feature is not added. If the feature is already added on v1, I would appreciate supporting it back on v2 even just an option as experimental as there is still a lot of issues related to OIDC implementation.

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.