Skip to content
This repository has been archived by the owner on Jan 26, 2021. It is now read-only.

Support for hybrid server-side flow (one-time codes) #5

Closed
taisph opened this issue Sep 13, 2014 · 29 comments
Closed

Support for hybrid server-side flow (one-time codes) #5

taisph opened this issue Sep 13, 2014 · 29 comments

Comments

@taisph
Copy link

taisph commented Sep 13, 2014

It would be nice if support for hybrid flows could be added. When a client authenticates with Google, it receives both an access token and a code which the client then sends to the client's server, who then exchanges that code directly with Google's servers to get its own access token. More secure than sharing the access token as you need the client secret to exchange the code server-side.

It seems like you can set response_type='token code' when calling gapi.auth.authorize to achieve this but I haven't found it explicitly documented.

Flow:

Using the Google+ signin button for the hybrid flow:
https://developers.google.com/+/web/signin/server-side-flow

@taisph
Copy link
Author

taisph commented Sep 13, 2014

The hybrid flow and response_type request behaviour is documented in OpenID Connect:
http://openid.net/specs/openid-connect-core-1_0.html

@sethladd
Copy link

/sub

@Scarygami
Copy link
Contributor

Related to having access to the code that can be sent to the server, on the server side you would need methods for obtainAccessCredentialsViaCodeExchange and clientViaCodeExchange.

Additionally a very common scenario for this flow is to save the refresh token obtained from the code exchange on the server side in order to be able to perform tasks on behalf of users while they are offline. This would use obtainAccessCredentialsViaCodeExchange and then store the refresh token together with a user id.

To use it, it would be necessary to have something like obtainAccessCredentialsViaRefreshToken and clientViaRefreshToken

@mkustermann
Copy link
Contributor

This seems to be a G+ only Sign in flow. The apps get also registered under https://plus.google.com/apps (normal apps get added to https://security.google.com/settings/security/permissions).

So far we only intended to implement OAuth2 flows from https://developers.google.com/accounts/docs/OAuth2.

Though it might make sense to add the G+ flow as well. We'll discuss this more.

@mkustermann
Copy link
Contributor

@Scarygami

Related to having access to the code that can be sent to the server, on the server side you would need methods for obtainAccessCredentialsViaCodeExchange and clientViaCodeExchange.

Very good point. We currently don't have server flows, but there is already a TODO in the code :)

Additionally a very common scenario for this flow is to save the refresh token obtained from the code exchange on the server side in order to be able to perform tasks on behalf of users while they are offline. This would use obtainAccessCredentialsViaCodeExchange and then store the refresh token together with a user id.

Correct. You could just save the (user-id, AccessCredentials object)-tuple to the DB. It is beneficial/important to add not just the refresh token because:
a) if the access token is still valid, there is no need to do a refresh round trip
b) it is/might be important to know which scopes a refresh token / access token (even though that can also be achieved by refresh-cycle+oauth2-tokeninfo-call)

To use it, it would be necessary to have something like obtainAccessCredentialsViaRefreshToken and clientViaRefreshToken

You could read the (user-id, AccessCredentials)-tuple from DB. If the AccessToken is still valid, you just use it, otherwise it needs to be refreshed.

There is already an autoRefreshingClient(ClientId, AccessCredentials, http.Client), which handles this under the hood.

There is also a refreshCredentials(ClientId, AccessCredentials, http.Client) if you want to do refresh manually.

@taisph
Copy link
Author

taisph commented Sep 18, 2014

@mkustermann I'm currently working on an app using hybrid flow as described. It does not get an entry in G+, only under account permissions as expected.

I currently do

      auth.callMethod('authorize', [new js.JsObject.jsify({
        'client_id': clientId,
        'scope': scopes.join(' '),
        'immediate': immediate,
        'response_type': 'code token id_token',
        'login_hint': loginHint,
        'cookie_policy': 'single_host_origin'
      }), (js.JsObject jsResponseObject) {

If successful it returns access_token, code and id_token (and several other params of course). Note: Cookie_policy allows Google to display the account chooser. Login_hint helps immediate logins, when there are multiple accounts to choose from.

The resulting code is then sent to my backend API which exchanges it with Google's OAuth API for an access_token (and refresh_token if required and allowed) for the server to use.

I only have a single client id registered on the developers console (Client ID for web application), and only the backend API knows and uses the client secret.

@mkustermann
Copy link
Contributor

@taisph Thanks for the info.

I've read this on the gplus web signin page where the hybrid flow is also described.

If this is a feature of the normal gapi.auth and not of the G+ specific one, we're more than happy to include the hybrid flow in this package. I'll try to reach out to people working on this and see if this is supported -- because as you mention, it doesn't seem to be documented here

In case you would like to contribute to the googleapis_auth package, I'm happy to pull changes. (See Google Contributor License Agreement for the formalities.)

@Scarygami
Copy link
Contributor

The "one-time code flow" is something that used to be specific to G+-sign-in, but the g+-signin flow has since been extended to be the general/preferred "Google sign-in" solution.

The gapi.auth.authorize docs are very incomplete as to parameters it accepts, but basically it takes any parameters you put in and constructs the query string from it. So any parameters listed in the OAuth2 docs can be put in there.

See https://developers.google.com/accounts/docs/OAuth2WebServer for all supported OAuth parameters.

The special "hybrid" flow is just a special implementation of the web-server flow by using redirect_uri: 'postmessage' so the flow completes on the client-side without redirecting to a special OAuth-callback-url (as was used in the past).

The important parameter for the code flow is access_type: 'offline' because otherwise the code won't give a refresh-token when exchanged on the server-side.

response_type normally doesn't need to be set explicitly, it will include token and code by default.

@mkustermann
Copy link
Contributor

I've spent more time looking into this and implemented a version of the hybrid flow in 8350d4a . Though, this hasn't been published on pub yet.

// client

createImplicitBrowserFlow().then((BrowserOAuth2 flow) {
  // Do this in user-interaction callback handler
  flow.runHybridFlow(forceUserConsent: true).then((HybridFlowResult result) {
    var code = result.authorizationCode;
    var credentials = result.credentials;
    var client = result.newClient();
  });
});

client sends "auth code" somehow to the server:

// server

obtainAccessCredentialsViaCodeExchange(..., code).then((AccessCredentials credentials) {
  // Maybe store [credentials] to DB

  // Construct HTTP autorefreshing HTTP client
  var client = autoRefreshingClient(clientId, credentials, httpClient);
});

We currently do not plan to handle "ID Tokens". Doing so would at least require code for verifying claims issued by the google endpoint.

Comments welcome -- it's not on pub and we can change the API if you've better suggestions.

@zoechi
Copy link

zoechi commented Oct 3, 2014

I tried it but I run into a weird problem

Exception: Uncaught Error: Class 'BrowserOAuth2Flow' has no instance method 'runHybridFlow'.

but when I navigate to the function using ctrl+click the method is shown.

@zoechi
Copy link

zoechi commented Oct 3, 2014

I had to change the path dependency to a git dependency.
No idea why this didn't work with path...

@zoechi
Copy link

zoechi commented Oct 3, 2014

Would be nice if AccessCredentials had a toJson() method and a fromJson() constructor to simplify send/receive.

@taisph
Copy link
Author

taisph commented Oct 3, 2014

I wrote a simple Google JWT claim decoder/validator (id_token; see link below). It still needs certificate signature validation but I haven't had the time to add this yet as the base JWT handler (dart_jwt) is still missing proper support for RS256. It does however validate the access token and code using the hashes provided in the JWT.

https://bitbucket.org/taisph/google-oauth2-jwt

@zoechi
Copy link

zoechi commented Oct 3, 2014

obtainAccessCredentialsViaCodeExchange(..., code).then((AccessCredentials credentials) {
returns AccessCredentials with accessToken set. I assume here the refreshToken should be set instead. Am I doing something wrong? Is the offline argument missing, where/how can I set it?
Because of the missing refreshToken
var client = autoRefreshingClient(clientId, credentials, httpClient); results in
Illegal argument(s): Refresh token in AccessCredentials wasnull.

@Scarygami
Copy link
Contributor

Getting a refreshToken for a code is only guaranteed during the first code exchange for a user, or to be more specific if the user sees and has to confirm a consent screen. The idea with the hybrid flow is that the first time the user logs in they give their consent and you request the refreshToken and then store it on your server for further use. When the user logs in the next time they will be logged in automatically without a consent screen, you can check their credentials via the code but you then use your stored credentials.

If the refreshToken gets "lost" at some point, the only way to get a new one is to call authentication with "approvalPrompt": "force", a parameter that is still missing from this auth library.

Reference: https://developers.google.com/+/web/signin/server-side-flow#step_6_send_the_authorization_code_to_the_server

@zoechi
Copy link

zoechi commented Oct 4, 2014

Hi Gerwin, thanks for your reply.
I use mostly the two code snippets above posted from @mkustermann.
I call runHybridFlow(forceUserConsent: true) on the client, send the acquired code to the server and call obtainAccessCredentialsViaCodeExchange on the server using this code.
This returns AccessCredentials with null for the refreshToken but var client = autoRefreshingClient(clientId, credentials, httpClient); needs the refreshToken to be set.
Any hint what I am missing?

@Scarygami
Copy link
Contributor

As I said, you only get a refreshToken on the very first code-exchange for a user. If you have authenticated before any future code-exchanges won't include the refreshToken.

@zoechi
Copy link

zoechi commented Oct 4, 2014

I don't get it. I send the code to the server and the server requests the token. How is that not the first code-exchange?

@Scarygami
Copy link
Contributor

When you sign-in, do you see a consent screen, or do you get logged in automatically?
If you don't see a consent-screen it isn't your first log-in and you won't get a refresh token for the code.

@Scarygami
Copy link
Contributor

The easiest way to test is to revoke permission for your client ID from https://security.google.com/settings/security/permissions and then try again.

The proper way would be to add an 'approvalPrompt': 'force' parameter during authentication, but that isn't possible in this library yet.

The complete hybrid-flow would look like this:

  1. User signs in for the first time with your app.
  2. Send code to server
  3. Exchange code for access token and refresh token
  4. Store those access credentials on the server.
  5. Create autoRefreshingClient using this refresh token whenever you need.
  6. User signs in again
  7. Send code to server
  8. Exchange code for access token
  9. Check if you already have a refresh token for that user and use it to get the autoRefreshingClient
  10. If you don't have/don't get a refresh token force user to sign-in again with 'approvalPrompt': 'force'

@zoechi
Copy link

zoechi commented Oct 6, 2014

Eine schwere Geburt ;-)
Got it working. Thanks a lot for your support!
Is there already an open issue to implement support for 'approvalPrompt': 'force' or what is the reason this is not yet supported?

@Scarygami
Copy link
Contributor

Best to open an issue for this. I guess the reason it is not yet supported, is that no-one needed it so far :)

@zoechi
Copy link

zoechi commented Oct 28, 2014

@mkustermann
Do you plan to publish the hybrid flow implementation anytime soon?

@mkustermann
Copy link
Contributor

[Sorry for the high latency, I've been on vacation for some time ...]

@zoechi
Yes I plan on fixing the "force" flag and pushing a new version of the package very soon, probably this week.

About "force: true":
We've discussed this a bit a couple of weeks ago. It is basically a 2x2 combination (immediate=true/false, force=true/false), where immediate=true/force=true doesn't make sense. immediate=false/force=false causes a popup window, but that might go away automatically if the user already gave consent (i.e. it flashes -- which is a pretty bad user experience).

Our discussion was about whether we should expose both flags, "immediate" and "force", or have only one boolean which switches between immediate=true/force=false and immediate=false/force=true.

@Scarygami Since you have experience in this area, can you think of a case where immediate=false/force=false provides any additional value?

@Scarygami
Copy link
Contributor

Actually immediate=false/force=false is the most common scenario you will use on your website, connected to a sign-in button.

The first thing you do is try a flow with immediate=true/force=false without any user-interaction to allow automatic login if you want to have that.
This immediate flow might not complete for a variety of reasons, e.g. if the user signed out or isn't logged into Google at all. You then display the sign-in button with immediate=false and force=false, this will sign-in users that have previously signed in (which causes the short popup, which is "normal" behaviour, but I think there's an issue for that for the JS client library since this needs to be solved on their side) but only displays the consent screen to users who have not giving permissions yet (or if there are additional permissions you are requesting).

immediate=false/force=true is actually something you would want to avoid, the only reason for this is if you have lost the refresh token and need users to sign-in again with consent-screen to get a code that allows you to retrieve a refresh token. For users who have signed in with your app previously this will create a rather confusing consent screen requesting only "offline access" because incremental auth doesn't display already granted permissions.

@mkustermann
Copy link
Contributor

Thanks Gerwin for the explanation.

CL is up for review to fix the force flag and add the immediate flag as well:
https://codereview.chromium.org/705773002/

@Scarygami
Copy link
Contributor

Looks good to me anyway :)

@mkustermann
Copy link
Contributor

We were already doing the right thing before in the normal flow -- but I've renamed the forceUserConsent to immediate -- because this is what it was actually doing.

immediate=false/force=true only make sense for offline access and we have the hybrid flow for that now.

Committed in ffbb5de & published googleapis_auth:0.2.0 on pub.

@zoechi
Copy link

zoechi commented Nov 6, 2014

Great, thanks!

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Development

No branches or pull requests

5 participants