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

Basic SSO Implementation #5

Closed
11 tasks
chpapa opened this issue Mar 28, 2017 · 44 comments
Closed
11 tasks

Basic SSO Implementation #5

chpapa opened this issue Mar 28, 2017 · 44 comments
Assignees
Milestone

Comments

@chpapa
Copy link
Contributor

chpapa commented Mar 28, 2017

Description

Have official plugins for common SSO support; And also make it easy to enable and config them from Skygear Portal.

Related Issues

Portal Design

Portal Features

  • List of official plugins:
    • Google
    • Facebook
    • Wechat
    • OAuth
  • Have an interface for on/off and configuration of ID / Secret.

UX Design

Put (link to) Portal Design here

API Design

Scenario

To be expanded

  • Sign in with web popup
  • Sign in with web redirect
  • Sign in with limited capability devices (TV/Command line)
  • Sign in with Mobile
  • Login by AuthProvider A, but the email already got another account from AuthProvider B, show an error and tell users to login with another AuthProvide B
  • Login by AuthProvider A, but the email already got another account from AuthProvider B, tell users to login and link with new AuthProvider
  • Login by AuthProvider A, but the email already got another account from AuthProvider B, assume it is two different accounts (will break the assumption of Skygear, which each users got unique email address)
  • At User Setting page, add another AuthProvider

Sample Code

Put sample code of how you vision this API will be used, consider different type of developers and different abstract level

API Design

This is the API I am proposing

JS

  • loginWithOAuthProvider(providerID, options, [accessToken])
    • Create or login a new skygear user, associated with the provider
    • providerID - A string that identify the login provider
    • options
      • uxMode - Either popup(default), or redirect
      • clientID
      • scope
      • redirectUrl - when uxMode is redirect, skygear will redirect the user to this url after auth. If it is null, back to the current URL
    • accessToken - Optional. Pass in access token if client already has it, skygear will try to login directly instead of going through the OAuth flow.
    • This function returns a skygear user, and an access token of the service.
  • associateAccountWithProvider(providerID, options)
    • Add a new auth provider to the user by going through the auth flow
    • This API requires user to be logged in already, return error otherwise
    • providerID - A string that identify the login provider
    • options
      • uxMode - Either popup(default), or redirect
      • clientID
      • scope
      • redirectUrl - when uxMode is redirect, skygear will redirect the user to this url after auth. If it is null, back to the current URL
    • This function returns a skygear user, and an access token of the new service.
  • getOAuthTokens()
    • Return a promise of tokens

      getOAuthTokens().then(function(tokens){
        //tokens['com.facebook'] is FB's access token
      });
      

Platform specific API

  • loginWithFacebook(options) and loginWithGoogle(options)
    • options
      • uxMode - Either popup(default), or redirect
      • clientID
      • scope
      • redirectUrl - when uxMode is redirect, skygear will redirect the user to this url after auth. If it is null, back to the current URL

iOS

  • -[SKYContainer loginWithOAuthProvider:(NSString*)providerID, options:(NSDictionary*)options completion:(void(^)(NSError*, SKYUser*))]
    • Create or login a new skygear user, associated with the provider
    • providerID - A string that identify the login provider
      • We will provide com.facebook, com.google
    • options
      • uxMode - Either popup(default), or redirect, popup means in-app-browser (SFSafariViewController/WKWebView) and redirect means Safari.app
      • clientID
      • scope
      • version - FB Client SDK only
      • cookiePolicy - Google Client SDK only
    • This function returns a skygear user, and an access token of the service, via a delegate.
  • -[SKYContainer loginWithOAuthProvider:(NSString*)providerID, accessToken:(NSString*)accessToken completion:(void(^)(NSError*, SKYUser*))]
    • accessToken - Client calls this API if it already has an access token, skygear will try to login directly instead of going through the OAuth flow.
  • -[SKYContainer associateAccountWithProvider:(NSString*)providerID options:(NSDictionary*)options completion:(void(^)(NSError*, SKYUser*))]
    • Add a new auth provider to the user by going through the auth flow
    • providerID - A string that identify the login provider
    • options
      • uxMode - Either popup(default), or redirect, popup means in-app-browser (SFSafariViewController/WKWebView) and redirect means Safari.app
      • clientID
      • scope
      • version - FB Client SDK only
      • cookiePolicy - Google Client SDK only
    • This function returns a skygear user, and an access token of the new service, via delegate.
  • -[SKYContainer getOAuthTokensWithCompletion:(void(^)(NSError*, NSDictionary*))]
    • Return tokens

      [container getOAuthTokensWithCompletion:^(NSDictionary *tokens){
        //tokens['com.facebook'] is FB's access token
      }];
      

Platform specific APIs

  • -[SKYContainer loginWithFacebook:(NSDictionary*)options] and -[SKYContainer loginWithGoogle:(NSDictionary*)options]
    • options
      • uxMode - Either popup(default), or redirect, popup means in-app-browser (SFSafariViewController/WKWebView) and redirect means Safari.app
      • clientID
      • scope
      • version - FB Client SDK only
      • cookiePolicy - Google Client SDK only
    • Returns the user logged in via delegate.

Skygear plugin

Environment variables

  • UNIQUE_EMAIL_FOR_ACCOUNTS
  • FACEBOOK_CLIENT_SECRET
  • GOOGLE_API_KEY
  • SSO_ENABLED e.g FACEBOOK,GOOGLE.

APIs

  • oauth:auth_url
    • Accepts a provider id and options
    • Return an url for auth
  • oauth:handle_code
    • Accepts code from 3rd party service
    • Exchange code with access token
    • Create user if needed
    • Pass the user back to client
  • oauth:handle_access_token
    • Accepts a provider id, and access token
    • If this handler is called with a user logged in
      • Associate the user with auth provider and access token
    • Otherwise,
      • Login or create new users according to the provider and access token
    • Return the user

Login flow

JS, iOS and Android should follow this flow:

When using 3rd party client

  1. Call 3rd party client
  2. When user is authed, get the access_token
  3. Pass access_token to oauth:handle_access_token, to receive skygear user
  • Server behaviour depends on UNIQUE_EMAIL_FOR_ACCOUNTS
  1. Return user and access_token

When using OAuth flow

  1. Ask for a url to display via oauth:auth_url
  2. Show the url to user, either popup or redirect
  3. After user login, the webpage should be redirected to skygear-server with a code
  4. Skygear-server exchange access token with code (oauth:handle_code)
  • Behaviour depends on UNIQUE_EMAIL_FOR_ACCOUNTS
  1. Skygear-server create or login a user
  2. Pass the user back to client side

For devices with limited capability

  1. Fetch code and URL from Google / Facebook
  2. Return the above data to the developer, expect the URL and should to be shown to user
  3. Constantly poll Google / Facebook for auth result
    • With timeout
  4. When access_token is received via polling, send it to skygear-server oauth:handle_access_token
  • Server behaviour depends on UNIQUE_EMAIL_FOR_ACCOUNTS
  1. Pass the user from skygear user and access_token back to user

Progress Tracker

Preparation

Implementation

  • Code
  • Minimal tests
  • API reference
  • Portal design implementation

QA

  • UI QA for portal design

Documentation

  • GitHub readme
  • Edit guides

Release

  • Release note
@b123400
Copy link
Contributor

b123400 commented Apr 21, 2017

I can think of 2 directions to implement:

1. Use FB/Google's client SDK

New client side APIs: container.loginWithFacebook container.loginWithGoogle.

They call 3rd party client API for auth, then pass the access token back to skygear-server via loginWithProvider.

Built-in providers:

  • com.oursky.facebook
  • com.oursky.google

These built-in providers would check if the access token is valid and create/login a skygear user.

2. Implement the entire oauth flow

New client side APIs: container.loginWithFacebook container.loginWithGoogle, which are alise of calling loginWithOAuthProvider with default argument.

Skygear will provide default OAuthProvider for Facebook and Google.
And people can create OAuthProvider by providing the following functions:

  • A function that returns an URL for auth, e.g. Facebook:

    @oauth_url('com.facebook')
    def facebook_oauth_url():
        return "https://www.facebook.com/v2.9/dialog/oauth?" +
               "client_id={read app-id from env}" +
               "&redirect_uri="http://example.com/oauth/facebook_auth"
    
  • A handler that handles the Oauth result

    @oauth_handler('oauth:facebook_auth', method=['GET'])
    def handle_oauth_success(code):
        access_token = exchange_code_for_access_token(code)
        # By default we can call the exisiting auth provider
        user = auth_provider('com.facebook', access_token)
        return user
    

If there is any app-id/api-key/secret/etc, read them from env variables.

Skygear client will get the above oauth_url from server and open in a popup. It will usually be an URL with request_token. After the oauth flow, skygear server would receive the access_token to perform login/sign up (handle_oauth_success in the above case).

Server then notifies client side (probably via postMessage, from popup to the main window) of the auth result. The notification code is automatically geneated inside @oauth_handler.

Pros and cons

1. Using 3rd party client SDK

  • Pros
    • Less things to implement

    • Since users are logged in using 3rd party client SDK, it is possible for them to call the SDK directly, e.g.

      skygear.loginWithFacebook().then(skygearUser=> {
        // They can call this because we are using FB.login internally
        FB.api('/me', (response)=>{...})
      })
      
      • Need to think more about this, we cannot guarantee anything because it is 3rd party. e.g. It wouldn't work if the user logged out of FB but still logged in skygear. We have to tell user the association between skygear and FB client SDK is not perfect.
  • Cons
    • Need to deal with versioning, what if user wants to use a different version of FB SDK, not the one we are supporting?
    • Need constant update to catch up with changes
    • Using 3rd party SDK = cannot control the exact behaviour, must follow the official user experience

2. Implement the entire Oauth flow

  • Pros

    • Easy to add support for other services with Oauth
    • Maybe we will need to implement the entire flow one day anyway, e.g. when we have to support a service without client SDK.
  • Cons

    • Access token is handled at server side, which means more work is needed if user wants to use 3rd party client SDK. For example if user wants to call FB API, he will need to write a lambda, because FB's client sdk does not has a setAccessToken function.

    • skygear.loginWithFacebook().then((skygearUser, fbAccessToken)=> {
        // we need this line, but they dont provide
        FB.setAccessToken(fbAccessToken);
        // So we cannot call this
        FB.api('/me', (response)=> {...});
      })
      

The second one is more generic, and can is suitable if we want to support many different services, seems like this is the case?. But if FB and Google (or the famous one with client SDK) are all we wants, the first one seems to be better?

I am personally for 2.

@chpapa
Copy link
Contributor Author

chpapa commented Apr 23, 2017

@b123400 I think 2 sounds better too. For one thing, we will definitely go beyond FB and Google very soon, implementing FB and Google SSO are just a small target to help us build the infrastructure and set the API design sample to implement further SSO.

Here are a few questions:

  1. You have mentioned there is no setAccessToken function, so more work is needed if user wants to use 3rd party client SDK , may you illustrate more the context? or give a sample of 3rd party client SDK? I don't quite get it sorry.

  2. You said Skygear client will get the above oauth_url from server and open in a popup , can we let the client chose to open in a popup or redirect? Redirect is getting more common these days as many browsers/plugins block popup.

  3. Can your design also consider the Scenario mentioned in the issue description? For instance:

    • Consider the case for sign-in with limited capability devices. Ex. someone use Skygear JS SDK to write Smart TV application, it seems at least Google, not sure FB login, provide another mechanism. And can we compatible with that?
    • Would sign in at mobile devices very different with JS? On mobile is it more common to use Client side SDK? Possible to have hybrid model?
    • How to handle the 3 multiple AuthProvide scenario mentioned?
  4. Can you also define the Scope and Parameter for FB / Google login we need to allow for configuration?

@b123400
Copy link
Contributor

b123400 commented Apr 23, 2017

You have mentioned there is no setAccessToken function, so more work is needed if user wants to use 3rd party client SDK , may you illustrate more the context? or give a sample of 3rd party client SDK? I don't quite get it sorry.

Sure. This is not exactly about auth, but a very common scenario with 3rd party login. Instead of just signing in, users may want to call the 3rd party service as well, for example fetch user information from Facebook.

Normally when you are only using the Facebook JS SDK, you do something like this:

FB.login(function() {
  FB.api('/me', function(response) {
    console.log('Good to see you, ' + response.name + '.');
  });
});

This works because FB.login internally saves the access token, then FB.api reads and uses the access token when sending the /me request.

Now, if we are implementing the entire OAuth flow, the skygear API would looks like this:

skygear.loginWithFacebook(function(skygearUser, fbAccessToken){
  // Now I have the access token
});

It's good that we have the access token, but how to use it? There is no public API for setting the access token into the internal of Facebook SDK.

Which means I have to do something like this:

FB.api('/me', function(response) {
  console.log('Good to see you, ' + response.name + '.');
}, {access_token: 'THE_ACCESS_TOKEN'});

As you can see, access token has to be passed to FB SDK for every request, which is not very convenient. Also, there is no guarantee 3rd party SDK would provide API to input access token, at least I couldn't find an API to pass access token in the Google Javascript API.

You said Skygear client will get the above oauth_url from server and open in a popup , can we let the client chose to open in a popup or redirect? Redirect is getting more common these days as many browsers/plugins block popup.

Sure. Though I don't think popup is an issue. Facebook's FB.login uses popup (no option to use redirect), Google's gapi.auth2.init uses popup as default, but with options to use redirect.

Consider the case for sign-in with limited capability devices. Ex. someone use Skygear JS SDK to write Smart TV application, it seems at least Google, not sure FB login, provide another mechanism. And can we compatible with that?

Would sign in at mobile devices very different with JS? On mobile is it more common to use Client side SDK? Possible to have hybrid model?

After some thoughts, Yes we can actually implement both flows. The web-based flow will always be available (I am not sure how limited an environment can be, but as long as they can open an URL, it should works.), and can try to use native mechanism to login by detecting the environment.

How to handle the 3 multiple AuthProvide scenario mentioned?

The existing AuthProvider should already be able to handle the scenarios.

@provides("auth", "com.facebook")
def provide_user(auth_data):
  existing_user = skygear.send_action('user:query', {'email': auth_data['email']}).result.get(0)

  # Case 1.
  # If the user don't want accounts to be merged
  if existing_user is not None:
    raise Exception('Email already used in another account')
  
  # Case 2.
  # If the user wants to merge manually
  # They will have to implement the function
  merge(existing_user, auth_data)
  return existing_user
  
  # Case 3.
  # If the user wants to treat them as different accounts,
  # Ask them to create anonymous account and save email else where, to workaround the email constraint
  new_user = skygear.send_action('auth:signup')
  # e.g. save email in user record instead of _user
  user_record = Record(
    id='user/'+new_user.id,
    email=auth_data['email'],
  })
  return new_user

Do you think it is reasonable to ask user to write the above code?

Can you also define the Scope and Parameter for FB / Google login we need to allow for configuration?

These are the params I can find in the document, not sure if more is needed in practice.

  • Facebook
    • client id
    • client secret
      • OAuth only
    • version
      • Client SDK only
    • scope
  • Google
    • Client ID
    • API Key
      • OAuth flow only
    • scope
    • cookie_policy
      • Client SDK only
    • prompt
      • consent
      • select_account
      • none
    • ux_mode
      • popup
      • redirect

By implementing both flows, it means when the user calls skygear.loginWithOAuthProvider():

  • Detect if there is a 3rd party SDK in the environment, if yes
    • Use the SDK to login user
    • Receive access token at client side
    • Pass the access token to server
    • Server merge the SSO user and return a skygear user
    • Pass skygear user back to user
  • if no
    • Ask for a url to display via oauth:auth_url
    • Show the url to user, either popup or redirect
    • After user login, the webpage should be redirected to skygear-server with a code
    • Skygear-server exchange access token with code
    • Skygear-server merge the SSO user and return a skygear user
    • Pass the user back to client side

@chpapa
Copy link
Contributor Author

chpapa commented Apr 24, 2017

@b123400 Cool!

Some follow-up questions/comments after our discussions today.

  1. For passing the access token between requests, I think it is mostly about cloud functions? Maybe we can store the access token in private user profile as a workaround. Need to coordinate with @ben181231 on Re-design User Profile Discoverability #48 ?
  2. Will need the API modification for redirect/pop-up OAuth of for JS SDK.
  3. We have discussed how to handle the 3 cases:
    • We will need to provide a portal / envvar setting, of whether the users want to allow multiple accounts per email for SSO (say UNIQUE_EMAIL_FOR_ACCOUNTS) if UNIQUE_EMAIL_FOR_ACCOUNTS is true, skygear-server shall put email address in auth_provider (if provided) into _user; And return error if email address are duplicated.
    • We will need another API for linking an existing user with a new AuthProvider
    • For sample scenario, the above should be able to fulfill these:
      • Different Authprovider = different account: Set UNIQUE_EMAIL_FOR_ACCOUNTS = false
      • Different Authprovider with identical email = same account: Set UNIQUE_EMAIL_FOR_ACCOUNTS = true, and developer should handle the email already taken error, by asking users to sign in and links the existing account with another SSO.
      • Unique Email with only one Authprovider, same case as above, but developers don't have to do anything special.

To finish this issue, can @b123400 you help:

  1. Please group everything and write all new API changes, sample code to illustrate use cases, and other works at the description?
  2. Please open issues related and put it in description.

@b123400
Copy link
Contributor

b123400 commented Apr 25, 2017

For passing the access token between requests, I think it is mostly about cloud functions? Maybe we can store the access token in private user profile as a workaround.

Oh this is nothing about cloud function, this is pure client side, browser only. The server always has access to the access token, and is indepenent on #48. On the server, everything is good.

The inconvenience is about Facebook JS SDK, on the client side, on the browser. For example, if you want to login the user then read profile from Facebook, you can implement with this:

FB.login(function() {
  FB.api('/me', function(response) {
    console.log('Good to see you, ' + response.name + '.');
  });
});

Notice there is no server side code involved, everything is browser only. It works by the browser sending a request directly to Facebook's server (with the access token), and get a response back. Look at the above code, there is no mention of the access token, that is because the library is saving it, and using it internally. FB.login saves the token, FB.api uses the token implicitly.

What happens if we implement the flow in Skygear? We have the access token, we can pass it to the browser, but because we are not Facebook, we cannot access the internal of the FB library.

skygear.loginWithFacebook(function(skygearUser, fbAccessToken){
  FB.api('/me', function(response) {
    console.log('Good to see you, ' + response.name + '.');
  });
});

This does not work, because FB.api does not know about the fbAccessToken. Instead, we have to do this:

skygear.loginWithFacebook(function(skygearUser, fbAccessToken){
  FB.api('/me', function(response) {
    console.log('Good to see you, ' + response.name + '.');
  }, {access_token: fbAccessToken});
});

Now, it works. There is an explicit access token passing, from Skygear JS SDK, to Facebook JS SDK.

This explicit access token passing is the inconvenience I was talking about.


API Changes

New API

I am just listing the JS API here for simplicity, similar APIs apply to both iOS and Android.

  • skygear.loginWithOAuthProvider(providerID, options)
    • providerID - A string that identify the login provider
      • We will provide com.facebook, com.google
    • options
      • uxMode - Either popup(default), or redirect
      • clientID
      • scope
      • version - FB Client SDK only
      • cookiePolicy - Google Client SDK only
    • This function returns a skygear user, and an access token of the service.

New server options (Editable on portal)

  • UNIQUE_EMAIL_FOR_ACCOUNTS - a boolean
  • OAuth secrets:
    • FACEBOOK_CLIENT_SECRET
    • GOOGLE_API_KEY

New cloud functions

  • Merging accounts
    • Register with @provides('oauth_merge')

    • It takes a skygear user, a new auth_data

    • It can throw exception to indicate there is an error

      @provides('oauth_merge')
      def merge_facebook(user, auth_data):
        raise Exception("Error!")
      
    • It can return an user to indicate the auth is successful

      @provides('oauth_merge')
      def merge_facebook(user, auth_data):
        return user
      
    • It can return a new user to indicate it is an separated user

      @provides('oauth_merge')
      def merge_facebook(user, auth_data):
        new_user = skygear.send_action('auth:signup')
        return new_user
      

Extra discussion

Do you think we should use com.google and com.facebook? Or com.oursky.google and com.oursky.facebook?

@chpapa
Copy link
Contributor Author

chpapa commented Apr 25, 2017

@b123400

  1. Understood for the access token. But seems still we need to have some ways to pass the access token to the client?
  2. For OAuth Secrets, do we need App ID too? Do we need to give a callback URL for users of Skygear to set it at Facebook / the other OAuth side? It seems these options are quite common.
  3. Should we have an ENABLE_FB_LOGIN and ENABLE_GOOGLE_LOGIN too?
  4. I don't think we need merging accounts as mentioned in last comments, as suggested, we need another Client SDK API to auth current logged in users with a new AuthProvider.
  5. I think we should call it com.skygear.google (or org.skygear.google?)

Also, can you also specify in your plan how it will support mobile SDK?

For device login / limited capability devices, take a look at:

@cheungpat
Copy link
Contributor

@chpapa wrote:

To finish this issue, can @b123400 you help:

  • Please group everything and write all new API changes, sample code to illustrate use cases, and other works at the description?
  • Please open issues related and put it in description.

I am under the impression that we should have a review of the API changes before creating issue for implementation.

@b123400
Copy link
Contributor

b123400 commented Apr 25, 2017

Understood for the access token. But seems still we need to have some ways to pass the access token to the client?

I expect access_token to be a part of the user record, so if the client can get the user, it can get the access_token as well. If you want it to be explicit, how about this, an API named oauth:access_token, it returns access tokens of different social networks of the client logged in.

skygear.getOAuthTokens(function(tokens){
  console.log(tokens['com.facebook']);
});

For OAuth Secrets, do we need App ID too?

Client ID should be enough. You need App ID elsewhere (e.g. on the Facebook config page), but skygear-server does not need App ID.

Do we need to give a callback URL for users of Skygear to set it at Facebook / the other OAuth side? It seems these options are quite common.

I originally think we should redirect back to the page the user came, but if a callback URL is better, sure it is possible.

Should we have an ENABLE_FB_LOGIN and ENABLE_GOOGLE_LOGIN too?

We can determind that from env variables, e.g. if FACEBOOK_CLIENT_ID is null, then Facebook is disabled. But if you think an boolean option is better, sure it is ok as well.

I don't think we need merging accounts as mentioned in last comments, as suggested, we need another Client SDK API to auth current logged in users with a new AuthProvider.

Do we need another client side API?

skygear.loginWithOAuthProvider('com.oursky.google').then(user=>{
  // now, the user is logged in Google
  // We want to login with a new AuthProvider
  skygear.loginWithOAuthProvider('com.oursky.facebook');
});
  • Server logged in user with Google
  • After that, when client calls skygear.loginWithOAuthProvider('com.oursky.facebook');, it also send the current user info
  • Server get the request, look at the user info, realise that, "A user who is signed in with Google also wants to sign in with Facebook"
  • When the auth with Facebook is finished, server now has both user info (received in the previous step), and a new access token
  • Server can add the new auth provider to user

I am under the impression that we should have a review of the API changes before creating issue for implementation.

OK, I didn't get that.

@chpapa
Copy link
Contributor Author

chpapa commented Apr 25, 2017

I am under the impression that we should have a review of the API changes before creating issue for implementation.

@cheungpat yes sorry I went too fast

@chpapa
Copy link
Contributor Author

chpapa commented Apr 25, 2017

@b123400 In responds to your comment:

I expect access_token to be a part of the user record, so if the client can get the user, it can get the access_token as well. If you want it to be explicit, how about this, an API named oauth:access_token, it returns access tokens of different social networks of the client logged in.

I have no preference on how explicit it should be, but I'm also not sure how to do it now. If you can include how we do it now / new API in the sample code on this issue that would be good for others to comment OR for yourself to decide how explicit it should be?

I originally think we should redirect back to the page the user came, but if a callback URL is better, sure it is possible.

Actually, I meant we should display OUR callback URL for users of Skygear to copy & paste. (Not an input field for users to set the callback URL) My impression is on Facebook / Google OAuth setting, one setting they required is the users should put a callback URL. Can you help verify?

We can determine that from env variables, e.g. if FACEBOOK_CLIENT_ID is null, then Facebook is disabled. But if you think a boolean option is better, sure it is ok as well.

Your suggestion seems fine. Or is there any existing convention @rickmak ?

Do we need another client side API?

Your suggestion sounds fine to me.

@b123400
Copy link
Contributor

b123400 commented Apr 26, 2017

After reading about devices with limited capability, this is the list of new API I propose.

JS

  • loginWithOAuthProvider(providerID, options)
    • Create or login a new skygear user, associated with the provider
    • providerID - A string that identify the login provider
      • We will provide com.facebook, com.google
    • options
      • uxMode - Either popup(default), or redirect
      • clientID
      • scope
      • version - FB Client SDK only
      • cookiePolicy - Google Client SDK only
      • redirectUrl - when uxMode is redirect, skygear will redirect the user to this url after auth. If it is null, back to the current URL
    • This function returns a skygear user, and an access token of the service.
  • addNewAuthProvider(providerID, options)
    • Add a new auth provider to the user by going through the auth flow
    • This API requires user to be logged in already, return error otherwise
    • providerID - A string that identify the login provider
    • options
      • uxMode - Either popup(default), or redirect
      • clientID
      • scope
      • version - FB Client SDK only
      • cookiePolicy - Google Client SDK only
      • redirectUrl - when uxMode is redirect, skygear will redirect the user to this url after auth. If it is null, back to the current URL
    • This function returns a skygear user, and an access token of the new service.
  • getOAuthTokens()
    • Return a promise of tokens

      getOAuthTokens().then(function(tokens){
        //tokens['com.facebook'] is FB's access token
      });
      

iOS

  • -[SKYContainer loginWithOAuthProvider:(NSString*)providerID, options:(NSDictionary*)options completion:(void(^)(NSError*, SKYUser*))]
    • Create or login a new skygear user, associated with the provider
    • providerID - A string that identify the login provider
      • We will provide com.facebook, com.google
    • options
      • uxMode - Either popup(default), or redirect
      • clientID
      • scope
      • version - FB Client SDK only
      • cookiePolicy - Google Client SDK only
      • redirectUrl - when uxMode is redirect, skygear will redirect the user to this url after auth. If it is null, back to the current URL
    • This function returns a skygear user, and an access token of the service.
  • -[SKYContainer addNewAuthProvider:(NSString*)providerID options:(NSDictionary*)options completion:(void(^)(NSError*, SKYUser*))]
    • Add a new auth provider to the user by going through the auth flow
    • providerID - A string that identify the login provider
    • options
      • uxMode - Either popup(default), or redirect
      • clientID
      • scope
      • version - FB Client SDK only
      • cookiePolicy - Google Client SDK only
      • redirectUrl - when uxMode is redirect, skygear will redirect the user to this url after auth. If it is null, back to the current URL
    • This function returns a skygear user, and an access token of the new service.
  • -[SKYContainer getOAuthTokensWithCompletion:(void(^)(NSError*, NSDictionary*))]
    • Return tokens

      [container getOAuthTokensWithCompletion:^(NSDictionary *tokens){
        //tokens['com.facebook'] is FB's access token
      }];
      

Skygear-server

Environment variables

  • UNIQUE_EMAIL_FOR_ACCOUNTS
  • ENABLE_FACEBOOK_LOGIN
  • ENABLE_GOOGLE_LOGIN
  • FACEBOOK_CLIENT_SECRET
  • GOOGLE_API_KEY

APIs

  • oauth:auth_url
    • Accepts a provider id and options
    • Return an url for auth
  • oauth:handle_code
    • Accepts code from 3rd party service
    • Exchange code with access token
    • Create user if needed
    • Pass the user back to client
      • If it's popup, use postMessage
      • If it's redirect, redirect it to url?skygear_user=...&access_token=...
        • Skygear JS should be able to pick up that query
        • I would like to avoid using URL query if possible, but need to consider the case of cross domain
  • oauth:handle_access_token
    • Accepts a provider id, and access token
    • If this handler is called with a user logged in
      • Associate the user with auth provider and access token
    • Otherwise,
      • Login or create new users according to the provider and access token
    • Return the user

Login flow

JS, iOS and Android should follow this flow:

When using 3rd party client

  1. Call 3rd party client
  2. When user is authed, get the access_token
  3. Pass access_token to oauth:handle_access_token, to receive skygear user
  • Server behaviour depends on UNIQUE_EMAIL_FOR_ACCOUNTS
  1. Return user and access_token

When using OAuth flow

  1. Ask for a url to display via oauth:auth_url
  2. Show the url to user, either popup or redirect
  3. After user login, the webpage should be redirected to skygear-server with a code
  4. Skygear-server exchange access token with code (oauth:handle_code)
  • Behaviour depends on UNIQUE_EMAIL_FOR_ACCOUNTS
  1. Skygear-server create or login a user
  2. Pass the user back to client side

For devices with limited capability

  1. Fetch code and URL from Google / Facebook
  2. Return the above data to the developer, expect the URL and should to be shown to user
  3. Constantly poll Google / Facebook for auth result
    • With timeout
  4. When access_token is received via polling, send it to skygear-server oauth:handle_access_token
  • Server behaviour depends on UNIQUE_EMAIL_FOR_ACCOUNTS
  1. Pass the user from skygear user and access_token back to user

@rickmak
Copy link
Member

rickmak commented Aug 28, 2017

Change regarding iOS11 worth a note for our implementation: openid/AppAuth-iOS#120

carmenlau added a commit to carmenlau/features that referenced this issue Aug 28, 2017
carmenlau added a commit to carmenlau/features that referenced this issue Aug 30, 2017
@chpapa
Copy link
Contributor Author

chpapa commented Sep 6, 2017

@carmenlau Thanks! Sorry I know it is really late and I have the following question regarding the SSO spec:

  1. So I assume for linkOAuthProviderWithPopup/Redirect function, even if the SSO emails address is different from the users account, it would still works?
  2. For native SDK (let's say FB SDK) login, am I correct that the users would call native SDK and call loginOAuthProviderWithAccessToken?
  3. Are there any benefits if we provide shorthand for loginWithFB() as an alias to loginOAuthProvider etc? Do we have parameters for specific SSO? or only the ID?

Finally, a small comment on the methodology is for API design, it is always good to include sample code for different use cases as a sanity check of how usable are the APIs.

@carmenlau
Copy link
Contributor

  1. So I assume for linkOAuthProviderWithPopup/Redirect function, even if the SSO emails address is different from the users account, it would still works?

In the design, only loginOAuthProvider will populate the user profile. And we will not change the existing user profile when calling linkOAuthProvider. So is the email the same doesn't matter.

  1. For native SDK (let's say FB SDK) login, am I correct that the users would call native SDK and call loginOAuthProviderWithAccessToken?

yes

  1. Are there any benefits if we provide shorthand for loginWithFB() as an alias to loginOAuthProvider etc?

If we have shorthand like loginWithFB, not sure will user expect we will call the native SDK for them. I think it is ok for adding the shorthand

Do we have parameters for specific SSO? or only the ID?

In the current design, we don't have specific SSO parameters. User only need to provide the provider name e.g. facebook, google in loginOAuthProvider

Finally, a small comment on the methodology is for API design, it is always good to include sample code for different use cases as a sanity check of how usable are the APIs.

Right, will try adding this when working on the SDK part.

@chpapa
Copy link
Contributor Author

chpapa commented Sep 7, 2017

@carmenlau thanks for the clarification.

@rickmak
Copy link
Member

rickmak commented Sep 8, 2017

Agreed implementation direction, discuss offline with @carmenlau

  1. Control flow will be written at plugin side. We will create plugin only endpoint for SSO purpose at skygear-server
  2. Reusing or keeping compatible with loginWithProvider(auth:login) is a non-goal in this features.
  3. _auth.auth may store raw data coming from the callback.
  4. We will provide a sensible default user profile value population for provider like google, facebook and github. What to include in default is pending discussion.
  5. We will provide hook to override the default user profile population.

@carmenlau
Copy link
Contributor

Discussion on the callback handling on JS SDK

After server obtained user information from 3rd party provider and created skygear access token. Server have to send the result back to the browser js sdk.

Solution 1:
Return 302 redirect and put the access token to url hash, reset the url by js sdk after consume. auth0 is using this approach.

  • Pros:
    - Both web, ios and android can use the same flow.
  • Cons:
    - Manipulating url might trigger state change event and affect the state of some single page app.
    - Security issue of browser cache.

Solution 2:
Return 200 and send the data to session, the page will redirect user back to client page url by js. In the client page, js sdk iframe a page with script will post back data to client page.

  • Pros:
    - Don't need to put extra information in url, will not affect user app state.
  • Cons:
    - Client need to clearly defined the white list domain in portal that can receive message in iframe, default will be localhost and skygear domain.
    - Have to maintain one more flow in SSO

Conclusion: We will use solution 2

@chpapa
Copy link
Contributor Author

chpapa commented Sep 22, 2017

if we choose solution 2, we shall make sure when we support custom domain (#95), the domain added there should also add to the default for the domain whitelist.

@carmenlau
Copy link
Contributor

The SSO portal design:
https://www.dropbox.com/s/tjk3xezufvbntwx/Skygear-portal-USER%20AUTH%20%28Social%20Login%29-050917.jpg?dl=0

The hover effect of SSO provider settings, siu tao gave us 2 version to choose. I prefer version B.
https://www.dropbox.com/s/ikjt66ddjnpr05e/Social%20Login-style-060917.jpg?dl=0

Remarks:

  • Change allow redirects url from input fields to textarea when implement

@chpapa
Copy link
Contributor Author

chpapa commented Oct 2, 2017

Related android SDK issue: SkygearIO/skygear-SDK-Android#73

@chpapa chpapa modified the milestones: Milestone 2, Milestone 3 Oct 8, 2017
chpapa pushed a commit to chpapa/features that referenced this issue Nov 1, 2017
chpapa pushed a commit to chpapa/features that referenced this issue Nov 1, 2017
chpapa pushed a commit to chpapa/features that referenced this issue Nov 1, 2017
chpapa pushed a commit to chpapa/features that referenced this issue Nov 1, 2017
rickmak added a commit to SkygearIO/skygear-SDK-JS that referenced this issue Nov 16, 2017
Supported flow are
- loginOAuthProviderWithRedirect
- loginOAuthProviderWithPopup
- linkOAuthProviderWithAccessToken
- unlinkAuthProvider

refs SkygearIO/features#5
@carmenlau carmenlau modified the milestones: Milestone 3, Milestone 4 Nov 22, 2017
@carmenlau carmenlau modified the milestones: Milestone 4, Milestone 5 Jan 16, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

7 participants