OAuth2.0 with Dart in a web browser
I was trying to access a Google GData API using Dart in a browser. More for fun than profit, but it posed a few challenges, so I thought I should document them.
The first step was to Authorize using OAuth2.0 which is the easiest way of Authorizing an application to use Google APIs. This is pretty trivial using server-side Dart, but I wanted to achieve the entire thing inside a web browser, and so this involves a different OAuth2.0 flow to the web-server flow that I am used to using, in this case the OAuth2.0 Client-side Application Flow.
The main difference with the web-server flow is that the entire OAuth2.0 dance occurs in a single step, i.e., there is no step to exchange a code for a token. This single step is performed by asking for a token in the initial redirect step with a response_type parameter of token.
Additionally, the client secret is not passed at any time, because this would not be safe information for the client application to give up to a web browser (since anyone could just read it).
The response is also different in that the token is returned in the URI fragment of the redirect, rather than as a URI parameter as would be returned in a web-server flow, so that after authorization the browser is redirected back to something like:
http://localhost:7890/DartOAuthTest.html#access_token=AHES6ZS2-t1ntxFsGsvfKbEFosG6nokTOrn9qwVysp_&token_type=Bearer&expires_in=3600
So, this is what we are aiming at:
Step 1: Generate the Authorization URI
Ok, so I'm sorry about that as it's mostly doing things that a decent URI library will handle, volunteers anyone? But we can see the parameters we will pass, and the important ones are the client_id, the scope, and the redirect_uri. The client_id and the redirect_uri should be configured in the Google API Console, and the scope is specific to whichever API(s) you will be using. In our case, we are using the Google Documents List API, with a scope of
https://docs.google.com/feeds/
Step 2: Open a popup window and redirect to the Authorization URI
We are now firmly in the realm of the DOM, and so once we have our redirect_uri, we open a window and send the client to it:
Step 3: Receive the redirect and extract the OAuth2.0 Authorization token
Once we have been redirected back to our application, we should use window.location to extract the Authorization token. Remember that this is in the fragment, and once again we would hope that Dart eventually has libraries to handle this extraction, but in our case, I did something nasty like:
Which probably won't work for 1000 edge cases, but you get the idea.
Once we have this token we need to somehow message it back to the parent window, which I was slightly puzzled how to do. I got some advice on Google+ and that was to use window.postMessage. It is a pretty nifty way of handling it, as the child window can access the parent as window.opener, and so the call simply becomes:
The first step was to Authorize using OAuth2.0 which is the easiest way of Authorizing an application to use Google APIs. This is pretty trivial using server-side Dart, but I wanted to achieve the entire thing inside a web browser, and so this involves a different OAuth2.0 flow to the web-server flow that I am used to using, in this case the OAuth2.0 Client-side Application Flow.
The main difference with the web-server flow is that the entire OAuth2.0 dance occurs in a single step, i.e., there is no step to exchange a code for a token. This single step is performed by asking for a token in the initial redirect step with a response_type parameter of token.
Additionally, the client secret is not passed at any time, because this would not be safe information for the client application to give up to a web browser (since anyone could just read it).
The response is also different in that the token is returned in the URI fragment of the redirect, rather than as a URI parameter as would be returned in a web-server flow, so that after authorization the browser is redirected back to something like:
http://localhost:7890/DartOAuthTest.html#access_token=AHES6ZS2-t1ntxFsGsvfKbEFosG6nokTOrn9qwVysp_&token_type=Bearer&expires_in=3600
So, this is what we are aiming at:
OAuth2.0 Client Flow (via http://code.google.com/apis/accounts/docs/OAuth2.html) |
Step 1: Generate the Authorization URI
String authEndpoint = 'https://accounts.google.com/o/oauth2/auth?'; String generateAuthUri(String client_id, String redirect_uri, String scope) { List<List<String>> params = [ ['client_id', client_id], ['redirect_uri', redirect_uri], ['scope', scope], ['response_type', 'token'], ['approval_prompt', 'force'] ]; return authEndpoint + generateQueryString(params); } // Hopefully, this will one day be handled in a library String generateQueryString(List<List<String>> params) { List<String> parts = new List<String>(); for (List<String> part in params) { String k = encodeComponent(part[0]); String v = encodeComponent(part[1]); parts.add(Strings.join([k, v], '=')); } return Strings.join(parts, '&'); } // Taken from Uri.dart String encodeComponent(String component) { if (component == null) return component; return component.replaceAll(':', '%3A') .replaceAll('/', '%2F') .replaceAll('?', '%3F') .replaceAll('=', '%3D') .replaceAll('&', '%26') .replaceAll(' ', '%20'); }
Ok, so I'm sorry about that as it's mostly doing things that a decent URI library will handle, volunteers anyone? But we can see the parameters we will pass, and the important ones are the client_id, the scope, and the redirect_uri. The client_id and the redirect_uri should be configured in the Google API Console, and the scope is specific to whichever API(s) you will be using. In our case, we are using the Google Documents List API, with a scope of
https://docs.google.com/feeds/
Step 2: Open a popup window and redirect to the Authorization URI
We are now firmly in the realm of the DOM, and so once we have our redirect_uri, we open a window and send the client to it:
void redirectAuth(String uri) { window.open(uri, 'authwin', 'width=600,height=400'); }Very simple, but the key will be how we handle the redirect back to us in the next stage.
Step 3: Receive the redirect and extract the OAuth2.0 Authorization token
Once we have been redirected back to our application, we should use window.location to extract the Authorization token. Remember that this is in the fragment, and once again we would hope that Dart eventually has libraries to handle this extraction, but in our case, I did something nasty like:
String extractToken() { // eww return window.location.hash.split('&')[0].split('=')[1]; }
Which probably won't work for 1000 edge cases, but you get the idea.
Once we have this token we need to somehow message it back to the parent window, which I was slightly puzzled how to do. I got some advice on Google+ and that was to use window.postMessage. It is a pretty nifty way of handling it, as the child window can access the parent as window.opener, and so the call simply becomes:
void sendTokenAndClose(String token) { window.opener.postMessage(token, '*'); window.close(); }Which needs to be caught by the parent window by registering an event handler for the message event:
window.on.message.add(tokenReceived, false); void tokenReceived(MessageEvent e) { // the token is in e.data }We are now ready to use our token to make API calls, probably using JSONP which is the topic of another blog post, but you can see an idea on how to do it here in the Dart mailing list.