Closed rishabhpoddar closed 3 years ago
We should consider using: https://github.com/ciaranj/node-oauth Also https://github.com/nextauthjs/next-auth/tree/canary/src/providers
@bhumilsarvaiya, there was a difference between providers we agreed on for the:
Please also include support for Twitter. I'll add support for Apple in the frontend.
We need to create tests applications for the thirdparty.demo.supertokens.io
application.
We will also need test applications for tests.
For tests applications, how do we store client secret for tests applications? Is that ok to store them publicly since it's only for testing locally or on CI, and will only be used by "supertokens-node/supertokens-auth-react" maintainers.
We need a way to manage those multiple tests application easily, i.e they should all be created from the same email address and devs should have a way to access them. A company address ideally, and a shared password.
For all the below authorisation URLs:
state
is set by the front end.clientID
and any additional paramsis added by the backend.redirect_uri
must match with one created for each application.Github:
[x] Test:
Apple:
Authorisation URL: https://appleid.apple.com/auth/authorize?response_type=code&id_token&response_mode=form_post&redirect_uri={REDIRECT_URI}&scope=name%20email&client_id={CLIENT_ID}&state={STATE}
Facebook:
Authorisation URL: https://www.facebook.com/v7.0/dialog/oauth?response_type=code&redirect_uri={REDIRECT_URI}&scope=email&client_id={CLIENT_ID}&state={STATE}
Google:
Twitter: Twitter seems like an hybrid between OAuth 1.0 and 2.0, not sure if we can implement this in a generic way. Maybe we should postpone handling twitter for now. https://developer.twitter.com/en/docs/authentication/oauth-2-0/application-only
@NkxxkN
For tests applications, how do we store client secret for tests applications? Is that ok to store them publicly since it's only for testing locally or on CI, and will only be used by "supertokens-node/supertokens-auth-react" maintainers.
Yes.
they should all be created from the same email address and devs should have a way to access them. A company address ideally, and a shared password.
Yea. Makes sense. WIll create them when we need access.
Twitter seems like an hybrid between OAuth 1.0 and 2.0, not sure if we can implement this in a generic way. Maybe we should postpone handling twitter for now
Agreed. NextJS auth handles OAuth 1.0 as well, which is what Twitter uses.. but we can do it later.
@NkxxkN For Google, authorization url should be: (as per this link)
https://accounts.google.com/o/oauth2/v2/auth?
scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email&
access_type=offline&
include_granted_scopes=true&
response_type=code&
state={STATE}&
redirect_uri={REDIRECT_URI}&
client_id={CLIENT_ID}
For Facebook, it will be v9.0 instead of v7.0
For rest of the providers, it is correct.
@bhumilsarvaiya state
should not be there from the response from the API.
@NkxxkN the redirect_uri
should be set by the backend. Only state is set on the frontend. But I leave it to you and @bhumilsarvaiya. Please be on the same page about what is set on the backend and frontend.
suggesting config for Google, Facebook, Github:
{
clientSecret: string,
clientId: string,
scope?: string[], // will be merged with what we have
accessTokenAPI?: {
requestBody?: any, // Will be merged with our object
requestHeaders?: any, // Will be merged with our object
requestQuery?: any // Will be merged with our object
},
authURL?: {
requestQuery?: any, // Will be merged with our object
requestHeaders?: any, // Will be merged with our object
}
}
For Apple:
{
clientSecret: {
teamId: string,
privateKey: string,
keyId: string
},
clientId: string,
scope?: string[], // will be merged with what we have
accessTokenAPI?: {
additionalBody?: any, // Will be merged with our object
additionalHeaders?: any, // Will be merged with our object
additionalQuery?: any // Will be merged with our object
},
authURL?: {
additionalQuery?: any, // Will be merged with our object
additionalHeaders?: any, // Will be merged with our object
}
}
Provider Type Suggestion:
{
id: string,
getAccessTokenFromAuthCode: (recipeInstance: Recipe, authCode: string): Promise<any>,
generateAuthorisationURL: (recipeInstance: Recipe) => Promise<string>,
getProfileInfo: (recipeInstance: Recipe, authCodeResponse: any) => Promise<{id: string, email?: {id: string, isVerified: boolean}}>
}
True! We can do redirect_uri
on the backend.
Another thing to notice is that most / some providers will refuse redirections to http
and enforce https
. Hence, making it hard to test locally. We'll have to make sure our examples are running with SSL locally. (HTTPS=true npm run start
seems to be working for the test app, I haven't tested others yet).
I updated the above comment with redirection URI for the test and dev apps.
Alternative way is to set up a proxy locally. Redirect URLs are actually one of the main reason for the success of the payed solution provided by https://ngrok.io. I hope we won't have to do it though.
HTTPS will only be needed for the website domain right? The API domain can still be http?
Yeah only front end
I spent the last 2h trying to handle Invalid SSL certs locally while running tests without success.
It seems that Google/Facebook/Github/Slack/Spotify Apple doesn't seem to accept it. No information found for Linkedin.
I suggest that we add both http://localhost:3031/auth/callback/{provider}
and https://localhost:3031/auth/callback/{provider}
when possible.
When developing locally, we can decide to use SSL or not.
When running end to end tests, we will only test providers that accepts redirecting to non SSL localhost.
We will also need to setup a test account connected for each social providers (with email/password in .env
) ?
That means we will only have tests for non-paying providers.
@bhumilsarvaiya why additionalBody
and requestBody
? What is the difference? Similar question for query and headers.
@NkxxkN ignore bhumil's eariler comment. It's not correct.
interface GoogleConfig {
clientId: string,
clientSecret: string,
scope?: string[] // default: ["https://www.googleapis.com/auth/userinfo.profile", "https://www.googleapis.com/auth/userinfo.email"]
}
interface FacebookConfig {
clientId: string,
clientSecret: string,
scope?: string[] // default: ["email"]
}
interface GithubConfig {
clientId: string,
clientSecret: string,
scope?: string[] // default: user
}
interface AppleClientSceret {
keyId: string;
privateKey: string,
teamId: string
}
interface AppleConfig {
clientId: string,
clientSecret: AppleConfig,
scope?: string[] // default: ["name", "email"]
}
interface Provider {
id: string;
version: "2.0"; // can be used if we later wants to support "1.0A" which is used by twitter
scope: string; // for non-custom providers, scope array will be unified to a single string based on how scopes are supposed to be passed for that particular provider
accessTokenAPI: {
url: string, // used to exchange auth code for access token
params: object // request body params
};
authorizationAPI: {
url: string; // base url for authroization url
params: object // request query params
};
clientId: string;
clientSecret: string | AppleClientSceret
getProfileInfo: (accessTokenAPIResponse: any) => Promise<any>
}
(authCodeFromRequest: string | undefined, redirectURI: string ) => {
id: string;
accessTokenAPI: {
url: string, // used to exchange auth code for access token
params: object // request body params
};
authorizationAPI: {
url: string; // base url for authroization url
params: object // request query params
};
getProfileInfo: (accessTokenAPIResponse: any) => Promise<any>
}
Google({
clientId: string,
clientSecret: string,
authorizationAPI?: {
params: object
}
}) => (
authCodeFromRequest: string | undefined,
redirectURI: string,
getClientSecret(...) => object | undefined
) => {
return {
id: "google",
accessTokenAPI: {
url: "https://accounts.google.com/o/oauth2/token"
params: {
code: authCodeFromRequest,
client_id: clientId,
client_secret: clientSecret,
redirect_uri: redirectURI,
grant_type: "authorization_code"
}
};
authorizationAPI: {
url: "https://accounts.google.com/o/oauth2/v2/auth",
params: {
scope: "https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email",
access_type: "offline",
include_granted_scopes: true,
response_type: "code",
redirect_uri: redirectURI,
client_id: clientId,
...authorizationAPI.params
}
};
getProfileInfo: async (accessTokenAPIResponse: any) => {
// TODO:
}
}
}
@rishabhpoddar
I created accounts and OAuth apps for Google/Facebook/Github. I can share details in private. @bhumilsarvaiya you might need that too.
For Apple, it does not seem possible to create an application for free for solo developers. You need to register to their developer program and provide company DUNS number and name (and pay 99$ from what I understand). Apple is always over complicated...
Should we skip Apple for now too and add bunch of other easier ones?
Should we skip Apple for now too and add bunch of other easier ones?
Yes please.. if there is a bug in apple's implementation, eventually the community will tell us :)
@NkxxkN I already have OAuth apps for Google and Facebook. I'll let you know when I need for Github. Thanks! 😀
@bhumilsarvaiya please coordinate with @NkxxkN to use the same accounts for testing for ease of maintenance.
Google(config: {
clientId: string,
clientSecret: string,
scope?: string[],
authorizationRedirect?: {
params?: object
}
}) => { // this function will also do other stuff like modify the scope etc..
id: "google",
get: (
redirectURI: string,
authCodeFromRequest: string | undefined
) => {
return {
accessTokenAPI: {
url: "https://accounts.google.com/o/oauth2/token"
params: {
code: authCodeFromRequest,
client_id: clientId,
client_secret: clientSecret,
redirect_uri: redirectURI,
grant_type: "authorization_code"
}
};
authorizationRedirect: {
url: "https://accounts.google.com/o/oauth2/v2/auth",
params: {
scope: "https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email",
access_type: "offline",
include_granted_scopes: true,
response_type: "code",
redirect_uri: redirectURI,
client_id: clientId,
...authorizationAPI.params
}
};
getProfileInfo: async (accessTokenAPIResponse: any) => {
// TODO: https://www.googleapis.com/oauth2/v1/userinfo?alt=json
}
};
}
}
Facebook(config: {
clientId: string,
clientSecret: string,
scope?: string[]
}) => {
id: "facebook",
get: (
redirectURI: string,
authCodeFromRequest: string | undefined
) => {
return {
accessTokenAPI: {
url: "https://graph.facebook.com/v9.0/oauth/access_token"
params: {
code: authCodeFromRequest,
client_id: clientId,
client_secret: clientSecret,
redirect_uri: redirectURI
}
};
authorizationRedirect: {
url: "https://www.facebook.com/v9.0/dialog/oauth",
params: {
scope: "email",
response_type: "code",
redirect_uri: redirectURI,
client_id: clientId,
...authorizationAPI.params
}
};
getProfileInfo: async (accessTokenAPIResponse: any) => {
// TODO: https://graph.facebook.com/me?access_token=
}
};
}
}
Github(config: {
clientId: string,
clientSecret: string,
scope?: string[],
authorizationAPI?: {
params?: object
}
}) => {
id: "github",
get: (
redirectURI: string,
authCodeFromRequest: string | undefined
) => {
return {
accessTokenAPI: {
url: "https://github.com/login/oauth/access_token"
params: {
code: authCodeFromRequest,
client_id: clientId,
client_secret: clientSecret,
redirect_uri: redirectURI
}
};
authorizationRedirect: {
url: "https://github.com/login/oauth/authorize",
params: {
scope: "user",
redirect_uri: redirectURI,
client_id: clientId,
...authorizationAPI.params
}
};
getProfileInfo: async (accessTokenAPIResponse: any) => {
// TODO: https://api.github.com/user | application/vnd.github.v3+json
}
};
}
}
Apple(config: {
clientId: string,
clientSecret: {
keyId: string;
privateKey: string,
teamId: string
},
scope?: string[],
authorizationRedirect?: {
params?: object
}
}) => {
id: "apple",
get: (
redirectURI: string,
authCodeFromRequest: string | undefined
) => {
// we generate the client secret everytime the function is called, and if authCodeFromRequest !== undefined
return {
accessTokenAPI: {
url: "https://appleid.apple.com/auth/token"
params: {
code: authCodeFromRequest,
client_id: clientId,
client_secret: generatedClientSecret, // client secret will be generated
redirect_uri: redirectURI,
grant_type: "authorization_code"
}
};
authorizationRedirect: {
url: "https://appleid.apple.com/auth/authorize",
params: {
scope: "name email",
redirect_uri: redirectURI,
client_id: clientId,
response_type: "code",
response_mode: "form_post",
...authorizationAPI.params
}
};
getProfileInfo: async (accessTokenAPIResponse: any) => {
// TODO:
}
};
}
}
@bhumilsarvaiya please coordinate with @NkxxkN to use the same accounts for testing for ease of maintenance.
Okay
@bhumilsarvaiya feedback on the config:
authorizationAPI
to something like authorizationRedirect
cause it's not an API exactly..authorizationAPI
is not needed in facebook, from the user's point of view (please check).@bhumilsarvaiya
userinfo.profile
, if the userId is there in userinfo.email
.What about email verification details for each of them?
- google sends the info in the userinfo API response
- facebook only returns email in the response body if the email is verified
- for github, an endpoint needs to be called
- For apple, the information whether or not the account has been verified can be obtained by decoding the id token got in the response when exchanging the auth code for user tokens. (link)
For facebook, what is the user experience for "Still, this field will not be returned if no valid email address is available. " ? Do they see an error from facebook's UI our UI should the error?
No error. Just for the https://graph.facebook.com/me API, email field won't be present in the response.
For google, we do not need userinfo.profile, if the userId is there in userinfo.email
Yes, we can do that. If the scope is only set to userinfo.email, the userinfo API will return four fields in the response: picture
, verified_email
(boolean), id
, email
.
What about userId value for each of the providers?
- for google, facebook and github, id is returned in their respective userinfo equivalent APIs.
- for apple, user's email is the apple id.
for apple, user's email is the apple id.
Doesn't apple provide some proxy email instead of the actual email? Are these lesser than 128 characters long?
Doesn't apple provide some proxy email instead of the actual email? Are these lesser than 128 characters long?
If user has opted for that, then yes. But if so, is_private_email
boolean obtained from decoded the idToken
will be true
.
@bhumilsarvaiya Are the fake email that apple generates lesser than 128 characters long?
Private relay email addresses have the following characteristics:
There is no mention regarding how long the email address length will be. The format is <unique-alphanumeric-string>@privaterelay.appleid.com
. For example, if j.appleseed@icloud.com is the Apple ID, an unique, random email address for a given app might look like dpdcnf87nu@privaterelay.appleid.com.
getProfileInfo: async (accessTokenAPIResponse: {
access_token: string,
expires_in: number,
token_type: string,
scope: string,
refresh_token: string
}) => {
let accessToken = accessTokenAPIResponse.access_token;
let authHeader = `Bearer ${accessToken}`;
let response = makeRequest({
url: "https://www.googleapis.com/oauth2/v1/userinfo",
params: {
alt: "json"
}
headers: {
"Authorization": authHeader
}
});
let userInfo = response.data;
let id = userInfo.id;
let email = userInfo.email;
let isVerified = userInfo.verified_email;
return {
id,
email: {
id: email,
isVerified
}
}
};
getProfileInfo: async (accessTokenAPIResponse: {
access_token: string,
expires_in: number,
token_type: string,
}) => {
let accessToken = accessTokenAPIResponse.access_token;
let authHeader = `Bearer ${accessToken}`;
let response = makeRequest({
url: "https://graph.facebook.com/me",
params: {
access_token: accessToken,
fields: "id,email",
format: "json"
}
});
let userInfo = response.data;
let id = userInfo.id;
let email = userInfo.email;
return {
id,
email: {
id: email,
isVerified: true
}
}
};
getProfileInfo: async (accessTokenAPIResponse: {
access_token: string,
expires_in: number,
token_type: string,
}) => {
let accessToken = accessTokenAPIResponse.access_token;
let response = makeRequest({
url: "https://api.github.com/user"
headers: {
"Authorization": authHeader,
"Accept": "application/vnd.github.v3+json"
}
});
let emailsInfoResponse = makeRequest({
url: "https://api.github.com/user/emails"
headers: {
"Authorization": authHeader,
"Accept": "application/vnd.github.v3+json"
}
});
let userInfo = response.data;
let emailsInfo = emailsInfoResponse.data;
let id = userInfo.id;
let email = userInfo.email;
let emailInfo = emailsInfo.find(e => e.email === email);
let isVerified = emailInfo !== undefined ? emailInfo.verified : false;
return {
id,
email: {
id: email,
isVerified
}
}
};
getProfileInfo: async (accessTokenAPIResponse: {
access_token: string,
expires_in: number,
token_type: string,
refresh_token: string,
id_token: string // JWT
}) => {
let payload = getTokenPaylaod(id_token);
let id = payload.email;
let isVerified = payload.email_verified;
return {
id,
email: {
id,
isVerified
}
}
};
@rishabhpoddar What error should be thrown to user if the accessTokenAPI returns with non-success status code?
What error should be thrown to user if the accessTokenAPI returns with non-success status code?
@bhumilsarvaiya you throw a general error which gets propogated to the user in their error handler. You do not send a 500 response yourself.
Recipe Module
Config normalisation:
disabledDefaultImplementation
: Only disables/auth
route. Can't disable/auth/callback/{ThirdPartyId}
.providers
: Throw an error if empty orlength === 0
.Types:
User
:https://github.com/supertokens/core-driver-interface/wiki#third-party-user
thirdPartyAuthCodeResponse
: Is the response from the third party provider when exchanging the auth codeTODO: