Sunday, January 15, 2012

OAuth2 provider instruction

I've decided to implement OAuth2 support in my web-site. Since my application is written in perl (based on Catalyst framework) I came to CPAN:
  • Found CatalystX::OAuth2::Provider, but it is too straightforward for my needs. And, looking through code, it seems to me not working at all.
  • There is a OAuth2::Lite, but marked as "beta", and not recommended for production use.
  • There is a Net::OAuth2 module, but it is a client only. 


So, I've decided that it'll be better to write my own implementation - just for my needs. And gone looking for samples / instructions.

I've start with reading oauth2 site -  not much, really, except for the link to RFC. Searching internet reveals some more articles, mostly about theory and some thoughts, and some server implementations. But... nothing like simple samples of realisation, or some code stub. Of course I can read other servers' code, but it is a bit annoying to decipher unknown web-framework written in not very familiar language. And so, I've read throughout RFC and made my own TODO list of what must be implemented in OAuth2 provider server.

My list is not universal, it is aimed to solve my particular situation. Also, when RFC allows different approaches I choose one, that suits me best.

Pre-register clients. Client registration includes:
  • client_id
  • password
  • uri, where user must be redirected to after access granted
  • logo, description
  • allowed scopes to request access to


authorize endpoint (/oauth/authorize)
  • Validate request:
    • find client by 'client_id' param - if not - show error page 'unauthorized_client' to user
    • if 'redirect_uri' param is present - check that it matches registered client pattern - if not - show error page to user - no redirect!
    • 'response_type' param == 'code' - if not - error = 'unsupported_response_type'
    • check that all substrings in 'scope' param are allowed for this client - if not - error = 'invalid_scope'
    • if 'state' param is present - save it in session
  • Authenticate user. Either get authentication information if already logged in, either provide ability to log in.
  • Show grants page with full description of grant request: client name, description, logo and list scopes.
  • If access not granted - redirect back with error = 'access_denied'.
  • If granted:
    • generate unique code string
    • store code with user_id, client_id, scopes, timestamp. If redirect_uri was in request - store it too.
    • form redirect url: redirect_uri from request OR client uri from registration, if 'state' was in request - add it, add list of scopes.


token endoint (/oauth/token)
  • Authenticate client (HTTP Basic) - get client_id, if not - error = 'invalid_client'
  • Check that 'grant_type' param == 'authorization_code', if not - error = 'unsupported_grant_type'
  • Find 'code' param value in DB. Check that client_id stored with code == client_id from authentication.
  • Ensure that code is not expired - stored timestamp > current timestamp - 60.
  • If 'redirect_uri' param is present in request - ensure that it is the same as stored with the code. If any of last three wrong - error = 'invalid_grant'
  • Create unique access token string, store it in DB (with client_id, user_id, timestamp, scopes).
  • Add headers to respond: Cache-Control: no-store and Pragma: no-cache
  • Respond with json {"access_token": token string, "token_type": "Bearer"}

Protected resource
And now, when you want to check that request is authorized to get information that belongs to user with 'user_id' and falls under scope 's1' you must:
  • check that 'Authorization' header is present
  • That it contains scheme "Bearer"
  • Decode base64 string, find access token in DB
  • Ensure, that this token allow access to user_id and scopes string contains 's1'

Few more words:
I assume that site supports some kind of per-user sessions.
Scope string is space-delimited list of any substrings, that could identify your site's sections or grants or any other kind of access restrictions.
'error' means that request must be redirected to redirection uri with param 'error'.
How to generate unique code and token strings is beyond this article, I think everyone has their favourite method.


Further reading:
I really think that it is very useful to read RFC - http://tools.ietf.org/html/draft-ietf-oauth-v2-22
And also RFC for Bearer scheme - http://tools.ietf.org/html/draft-ietf-oauth-v2-bearer-15
If now you want to add support for other flows - it'll be much easier.


It is not still implemented, just work-in-progress. I hope that I didn't make too much mistakes. Comments, advices and criticism are appreciated.