Friday, April 19, 2013

Sign in with Google - Using the Google OAuth2 pub package for sign-in

Summary of "Sign in with Google - Using Dart's Google OAuth2 pub package for sign-in" (via tldr.io)

  • Using OAuth2 in Dart is easy, but you need to register for APIs first on at the Google API Console site and create a Client ID
  • Then import the Oauth2 pub package into your google project and use your Client ID
  • When you call the login() function, the user gets prompted for Google Login details, and you get an oauth2 token and the users email.
  • You can use this info in your application, but make sure you verify the token on the server-side as well
  • To retrieve other info, use the token to call other Google APIs, such as Google Plus profile API.

Adam Singer and Gervin Sturm have been updating their Dart Google APIs for Milestone 4, so I thought I'd take this opportunity to show how use the OAuth2 pub package to authenticate your browser app with Google.

Signed in to our application.

Use case

This is a simple use-case: When a user visits my web app, I want the user to "register" get access to some of the features, mainly so that I can show unique content for that user and send occasional emails. All I really need is the user's (verified) email and their name - once I have that information, I can do what I want with it, including sending it to my server, showing custom content etc.

The OAuth2 pub package lets you authenticate a user, without needing to worry about managing or storing passwords on your own website.   Once the user is authenticated (and you have an oauth2 token), you can use that token to access other Google services on behalf of that user.

Note: This post only follows the happy path for example only - there is no error handling in here.  You should ensure that you add error handling as appropriate for your application.

Registering for APIs

Before you can start playing with OAuth2, you need to register for a Google API.
Head over to https://code.google.com/apis/console/ and you should see something like the image below:
Google API console
You should choose "Create" to create a new API Project. I'm calling this project "Dartwatch OAuth Blogpost".   Once you've entered a project name, go to the API Access screen shown below:
Create an OAuth 2 client Id
Clicking on the big button, and you'll start the two-step wizard for creating your client ID.  On the first screen, the only mandatory field is the project name (you can edit the other fields later).  
Step 1
On the second step, shown below, you need to enter your site's hostname.  Because we're going to be building this locally using the Dart Editor, for now, just make sure it says http://127.0.0.1:3030 (which is the URL that the Dart Editor uses to launch web-apps.  You can add more later.

Step 2

Once you've done that, you'll be presented with a screen that shows your Client ID.  Keep this screen open, because we'll use that value in a minute.

Creating a project

Start up the Dart Editor, and create a new "Web Application", (I'm calling my project "signin" and clear out the existing sample code from "signin.dart" (you can leave the sample html as-is).

Using the OAuth2 pub package

Open your pubspec.yaml file, and add the google_oauth2_client with a version value of >=0.2.2 



The Dart Editor will run pub and import that package.  Now you add the package import into your code, shown below:


1
2
3
4
5
6
import 'dart:html';
import 'package:google_oauth2_client/google_oauth2_browser.dart';

void main() {

}


Let's make use of the OAuth client by adding it into our code.  We'll create an auth variable initialized with our oauth Client ID that we registered earlier.  We're going to pass in an oauthReady function callback that is called once we have the oauth token (and just print that token for the moment).


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import 'dart:html';
import 'package:google_oauth2_client/google_oauth2_browser.dart';

void main() {

}

final auth = new GoogleOAuth2(
    "4xxxxxxxxxxx.apps.googleusercontent.com", // Client ID
    ["openid", "email"],
    tokenLoaded:oauthReady);

void oauthReady(Token token) {
  print(token);
}

The values ["openid", "email"] represent "scopes" that we want to access.  You need at-least the openid scope, if you need to access the users email, then also request the email scope, these scopes affect what the oauth token is valid for (later, we'll add a Google+ Profile scope).

A scope is often a URI, such as https://www.googleapis.com/auth/plus.me or https://www.googleapis.com/auth/drive - you can checkout the oauth2 playground for a more complete list of scopes.


Let's login.

To start with, I'm going to add a "Sign in with Google" button, and display the logged in users email address.  In the main function, we need to call auth.login();  (which will be inside a button click).

1
2
3
4
5
6
7
8
9
void main() {
  var signIn = new ButtonElement();
  signIn.text = "Sign in with Google";
  signIn.onClick.listen((_) {   
    auth.login();
  });

  document.body.children.add(signIn);
}


When the user clicks this button, they will be prompted with a Google Login screen, shown below (If they use 2 factor authentication, then it will also prompt them for the passcode).

They will be asked to grant access to your application, and given a summary of the information that you want to get (based upon the scopes asked for).

Once signed in, the oauthReady(token) function is called, and you have an oauth token. The print statement in our oauthReady function outputs text similar to that shown below (I've formatted it for readability and x'd out some of the info):

[Token 
  type=Bearer, 
  data=yxxx.AxxxxxxxxxxxxB-6xxxxxM_xxx-Ixxxxxxxxxxxxxxxxxxb,  
  expired=false, 
  expiry=2013-04-19 20:25:39.513, 
  email=cxxxxxxxxxxx@gmail.com, 
  userId=xxxxxxxxx5xxxxxxxxxxx
]

The token is valid for about an hour, and contains the users email address and a user id. The userId can be used as a unique key for this user.

Using the token

From this point, it depends upon how you want to use this information.  You could send the userId and email address to your server to determine if the user has already registered.  If not, your UI could prompt them to gather extra details, or display alternate content.

Important warning:  If you are sending token data from the client to the server, you must ensure that you validate the token on the server - otherwise you cannot be sure that has not been modified en-route.  Google provide a service that lets you validate the token.data:
https://www.googleapis.com/oauth2/v1/tokeninfo?access_token=yxxx.AxxxxxxxxxxxxB-6xxxxxM_xxx-Ixxxxxxxxxxxxxxxxxxb
This returns a block of JSON data containing the email address, the userId, the scope's that the token is valid for, and how long until it is expired.  

Pulling information from Google+

I'm going to use that token to access Google+ Profile data and display the users full name (note, you can also get this from the users Google profile rather than Google+).

To access Google+, you need to enable API access.  Head back over to the API Console again, and bring up the

Enable Google+ API Access
Then head to the API access screen and create a "Simple API Access" browser key (if there's not one there already):

Create simple API access
You'll need to use the API key that's created in the next step.

Adding the Google+ API Pub Package

Head back over to your pubspec.yaml, and add google_plus_v1_api with a version of any.
Then we need to import that package in our code, and add the scope URI (which is handily provided as a constant within the package).

1
2
3
4
5
6
import "package:google_plus_v1_api/plus_v1_api_browser.dart" as plusclient;

final auth = new GoogleOAuth2(
    "333043597260.apps.googleusercontent.com",
    ["openid", "email", plusclient.Plus.PLUS_ME_SCOPE],
    tokenLoaded:oauthReady);

In our oauthReady() function, we'll add more code to set our API key and the OAuth token, and us the Google+ API to get information about "me" (me being the person authorized).  The structure of this API call is documented here, but the person.name output looks like this:
{"familyName":"Buckett","givenName":"Chris"}

Here's the code for the modified oauthReady() function:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
void oauthReady(Token token) {
  // get the users full name
  var plus = new plusclient.Plus(auth);
  // set the API key
  plus.key = "Axxxxxxxxxxxxxxxxxxx-x-xxxxxxxxx-x";
  plus.oauth_token = auth.token.data;
  plus.people.get("me").then((person) {
    // log the users full name to the console
    print("Hello ${person.name.givenName} ${person.name.familyName}");
  });  
}


And that's pretty much it - we can display this info back in our UI as a nice welcome message, or use it elsewhere to provide a richer experience for the user.



Hint: If you want to provide a way for the user to logout, you can call auth.logout()


Resources:

Google API discovery service: https://developers.google.com/discovery/
Google OAuth2 service playground: https://developers.google.com/oauthplayground/

As ever, comments are welcome, and if you find any errors with this post, please contact me via my Google+ profile

7 comments:

  1. When I tried, but I got error saying "no registered origin". I registered a client, got a client ID which I used and is displayed in the error details as below. When I registered client, I was given an API key, but in your example, I did see where the API key is used. Thanks.

    Error: invalid_client
    no registered origin
    Request Details
    openid_connect_request=true
    scope=https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/plus.me
    response_type=token
    access_type=online
    redirect_uri=postmessage
    origin=http://127.0.0.1:3030
    display=page
    client_id=.apps.googleusercontent.com

    ReplyDelete
  2. Found the cause of the error, I did not set the JavaScript origins in "API access" . You mentioned this in Usage/Installation section. Once I did this, it worked. Thanks.

    ReplyDelete
  3. This comment has been removed by the author.

    ReplyDelete
    Replies
    1. Hi - did you complete the steps in Registering for APIs - specifically creating a client-id. This is probably because the URL that you are running the app from is different to what is registered in the google API console.

      Delete
    2. Thanks for your help.after I edit the default client setting as my running application Url ,it work!!!!

      Delete
  4. Great post, thanks Chris. What's awesome is all the libraries that are popping up. Already in the past year I would assert Dart is overtaking js in usable libraries where the community will start by looking for a pre-existing solution while js is not seen that way.

    Great stuff, keep up the good work, we very much appreciate it.

    ReplyDelete