LWP::Authen::OAuth2::Overview(3pm) | User Contributed Perl Documentation | LWP::Authen::OAuth2::Overview(3pm) |
LWP::Authen::OAuth2::Overview - Overview of accessing OAuth2 APIs with LWP::Authen::OAuth2
This attempts to be the document that I wished existed when I first tried to access an API that used OAuth 2 for authentication. It explains what OAuth 2 is, how it works, what you need to know to use it, and how LWP::Authen::OAuth2 tries to make that easier. It hopefully also explains this in a way which will help you read documentation written by other people who assume that you have this knowledge.
Feel free to read as much or little of this document as makes sense for you. It is not actually designed to be read in a single sitting.
Since part of the purpose of this document is to familiarize you with the jargon that you're likely to encounter, all terms commonly used in discussions of OAuth 2 with a specific meaning are highlighted. Terms will hopefully be clear from context, but all highlighted terms are explained in the "Terminology" section.
OAuth 2 makes it easy for large service providers to write many APIs that users can securely authorize third party consumers to use on their behalf. Everything good (and bad!) about the specification comes from this fact.
It therefore specifies an authorization handshake through which permissions are set up, and then a message signing procedure through which you can then access the API. Well, actually it specifies many variations of the authorization handshake, and multiple possible signing procedures, because large organizations run into a lot of use cases and try to cover them all. But conceptually they are all fundamentally similar, and so have been lumped together in one monster spec.
LWP::Authen::OAuth2 exists to help Perl programmers who want to be a consumer of an API protected by OAuth 2 to construct and make all of the necessary requests to the service provider that you need to make. You will still need to set up your relationship with the service provider, build your user interaction, manage private data (hooks are provided to make that straightforward), and figure out how to use the API.
If that does not sound like it will make your life easier, then this module is not intended for you.
If you are not a consumer, this module is definitely not intended for you. (Though this document may still be helpful.)
OAuth 2 allows a user to tell a service provider that a consumer should be allowed to access the user's data through an API. This permissioning happens through the following handshake.
The consumer sends the user to an authorization_url managed by the service provider. The service provider tells the user that the consumer wants access to that account and asks if this is OK. The user confirms that it is, and is sent back to the consumer with proof of the conversation. The consumer presents that proof to the service provider along with proof that it actually is the consumer, and is granted tokens that will act like keys to the user's account. After that the consumer can use said tokens to access the API which is protected by OAuth 2.
All variations of OAuth 2 follow this basic pattern. A large number of the details can and do vary widely. For example JavaScript applications that want to make AJAX calls use a different kind of proof. Applications installed on devices without web browsers will pass information to/from the user in different ways. And each service provider is free to do many, many things differently. The specification tries to document commonalities in what different companies are doing, but does not mandate that they all do the same thing.
(This sort of complexity is inevitable from a specification that tries to make the lives of large service providers easy, and the lives of consumers possible.)
If you want to access an OAuth 2 protected API, you need to become a consumer. Here are the necessary steps, in the order that things happen in.
The redirect_uri is often a "https:///..." URL under your control. You also are likely to have had to tell the service provider about what type of software you're writing (webserver, command line, etc). This determines your client type. They may call this a scenario, or flow, or something else.
You will also need information about the service provider. Specifically you will need to know their Authorization Endpoint and Token Endpoint. They hopefully also have useful documentation about things like their APIs.
LWP::Authen::OAuth2 is not directly involved in this step.
If a LWP::Authen::OAuth2::ServiceProvider::Foo class exists, it should already have the service provider specific information, and probably has summarized documentation that may make this smoother. If you're really lucky, there will be a CPAN module (or modules) for the API (or APIs) that you want to use. If those do not exist, please consider creating them.
If no such classes exist, you can still use the module. Just pass the necessary service provider facts in your call to "LWP::Authen::OAuth2->new(...)" and an appropriate LWP::Authen::OAuth2::ServiceProvider will be created for you on the fly.
LWP::Authen::OAuth2 does not address this, beyond providing hooks that you are free to use as you see fit.
LWP::Authen::OAuth2 helps you build that URL. The rest is up to you.
If you succeeded, you will receive a code in some way. For instance if your redirect_uri is a URL, it will have a get parameter named "code".
You could get an "error" parameter back instead. See RFC 6749 <http://tools.ietf.org/html/rfc6749#section-4.1.2.1> for a list of the possible errors. Note that there are possible optional fields with extra detail. I would not advise optimism about their presence.
LWP::Authen::OAuth2 is not involved with this.
NOTE that the code cannot be expected to work more than once. Nor can you expect the service provider to repeatedly hand out working codes for the same permission. (The qualifier "working" matters.) Being told this will hopefully let you avoid a painful debugging session that I did not enjoy.
LWP::Authen::OAuth2 will perform this refresh/retry logic for you automatically if possible, and provides a hook for you to know to save the updated token data.
Some client types are not expected to use this pattern. You are only given an access token and are expected to send the user through the handshake again when that expires. The second time through the redirect on the service provider's side is immediate, so the user experience should be seamless. However LWP::Authen::OAuth2 does not try to automate that logic. But "$oauth2->should_refresh" can let you know when it is time to send the user through, and "$oauth2->can_refresh_tokens" will let you know whether automatic refreshing is available.
Note that even if it is available, retry success is not guaranteed. The user may revoke your access, the service provider may decide you are a suspicious character, there may have been a service outage, etc. LWP::Authen::OAuth2 will throw errors on these error conditions, handling them is up to you.
This section is intended to be used in one of two ways.
The first option is that you can start reading someone else's documentation and then refer back to here every time you run across a term that you do not immediately understand.
The second option is that you can read this section straight through for a reasonably detailed explanation of the OAuth 2 protocol, with all terms explained. In fact if you choose this option, you will find it explained in more detail than you need to be a successful consumer.
However if you use it in the second way, please be advised that this does not try to be a complete and exact explanation of the specification. In particular the specification requires specific error handling from the service provider that I have glossed over, and allows for extra types of requests that I also glossed over. (Particularly the bit about how any service provider at any time can add any new method that they want so long as they invent a new grant_type for it.)
Where I have a choice in this document I say consumer rather than client because that term is less likely overloaded in most organizations.
I chose to say user instead of Resource Owner because that is my best guess as to what the consumer is most likely to already call them.
The consumer does not need to care about this distinction, but it exposes an important fact about how the service provider is likely to be structured internally. You typically will have one team that is responsible for granting access, tracking down clients that seem abusive, and so on. And then many teams are free to create useful stuff and write APIs around them, with authorization offloaded to the first team.
As a consumer, you will make API requests to the Resource Server signed with proof of auhorization from the Authorization Server, the Resource Server will confirm authorization with the Authorization Server, and then the Resource Server will do whatever it was asked to do.
Organizing internal responsibilities in this manner makes it easier for many independent teams in a large company to write public APIs.
Of course all of this is up to the service provider. For example at the time of this writing, Google documents no less than six client types at <https://developers.google.com/accounts/docs/OAuth2>, none of which have been given the above names. (They also call them "Scenarios" rather than client type.) They rename the top two, split native application into two based on whether your application controls a browser, and add two new ones.
Despite flow being more common terminology in OAuth 2, client type is more self-explanatory, so I've generally gone with that instead.
It is up to the service provider what values of are acceptable for the redirect_uri, and whether it is a piece of information that is remembered or passed in during the authorization process.
Inside of the service provider, what likely happens is that the team which runs a given Resource Server tells the team running the Authorization Server what permissions to their API should be called. And then the Authorization Server can limit a given consumer to just the APIs that the user authorized them for.
While the field is not very useful for Perl clients, it is required in the specification. So you have to pass it.
It is constructed as the Authorization Endpoint with get parameters added for the response_type, client_id, and optionally state. The specification mentions both redirect_uri and scope but does not actually mandate that they be accepted or required. However they may be. And, of course, a given service provider can add more parameters at will, and require (or not) different things by client type.
An example URL for Google complete with optional extensions is <https://accounts.google.com/o/oauth2/auth?scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile&state=%2Fprofile&redirect_uri=https%3A%2F%2Foauth2-login-demo.appspot.com%2Fcode&response_type=code&client_id=812741506391.apps.googleusercontent.com&approval_prompt=force>
In LWP::Authen::OAuth2 the "authorization_url" method constructs this URL. If your request needs to include the state, scope, or any service provider specific parameter, you need to pass those as parameters. The others are usefully defaulted from the service provider and object.
In all interactions where it is passed it is simply called the code. But it is described in one interaction as an authorization_code.
We will later encounter the grant_type "refresh_token". The specification includes potential requests that can be in a flow that might prove useful. However you are only likely to encounter that if you are subclassing LWP::Authen::OAuth2::ServiceProvider. In that case you will hopefully discover the applicability and details of those grant_types from the service provider's documentation.
An example request might look like this:
POST /o/oauth2/token HTTP/1.1 Host: accounts.google.com Content-Type: application/x-www-form-urlencoded code=4/P7q7W91a-oMsCeLvIaQm6bTrgtp7& client_id=8819981768.apps.googleusercontent.com& client_secret={client_secret}& redirect_uri=https://oauth2-login-demo.appspot.com/code& grant_type=authorization_code
and the response if you're lucky will look something like:
HTTP/1.1 200 OK Content-Type: application/json;charset=UTF-8 Cache-Control: no-store Pragma: no-cache { "access_token":"1/fFAGRNJru1FTz70BzhT3Zg", "expires_in":3920, "token_type":"Bearer", "refresh_token":"1/xEoDL4iW3cxlI7yDbSRFYNG01kVKM2C-259HOF2aQbI" }
or if you're unlucky, maybe like this:
HTTP/1.1 200 OK Content-Type: application/json;charset=UTF-8 Cache-Control: no-store Pragma: no-cache { "error":"invalid_grant" }
Success is up to the service provider which can decide not to give you tokens for any reason that they want, including that you asked twice, they think the user might be compromised, they don't like the client, or the phase of the Moon. (I am not aware of any service provider that makes failure depend on the phase of the Moon, but the others are not made up.)
The "request_tokens" method of LWP::Authen::OAuth2 will make this request for you, read the JSON and create the token or tokens. If you passed in a "save_tokens" callback in constructing your object, that will be called for you to store the tokens. On future API calls you can retrieve that to skip the handshake if possible.
See LWP::Authen::OAuth2::AccessToken for advice on how to add support for a new or incorrectly implemented token_type.
Once the authorization handshake is completed, if the access_token has a supported token_type. then LWP::Authen::OAuth2 will automatically sign any requests for you.
Authorization: Bearer 1/fFAGRNJru1FTz70BzhT3Zg
You can also sign by passing "access_token=..." as a post or get parameter, though the specification recommends against using a get parameter. If you are using LWP::Authen::OAuth2, then it is signed with the header.
Thus in the above case we'd send
POST /o/oauth2/token HTTP/1.1 Host: accounts.google.com Content-Type: application/x-www-form-urlencoded refresh_token=1/xEoDL4iW3cxlI7yDbSRFYNG01kVKM2C-259HOF2aQbI& client_id=8819981768.apps.googleusercontent.com& client_secret={client_secret}& grant_type=refresh_token
and if lucky could get a response like
HTTP/1.1 200 OK Content-Type: application/json;charset=UTF-8 Cache-Control: no-store Pragma: no-cache { "access_token":"ya29.AHES6ZSiArSow0zeKokajrri5gMBpGc6Sq", "expires_in":3600, "token_type":"Bearer", }
and if unlucky could get an error as before.
In LWP::Authen::OAuth2 this request is made for you transparently behind the scenes if possible. If you're curious when, look in the source for the "refresh_access_token" method. There are also optional callbacks that you can pass to let you save the tokens, or hijack the refresh method for your own purposes. (Such as making sure that only one process tries to refresh tokens even though many are accessing it.)
But note that not all flows offer a refresh_token. If you're on one of those flows then you need to send the user back to the service provider for authorization renewal. From the user's point of view this is likely to be painless because it will be done with transparent redirects. But the consumer needs to be aware of it.
Ben Tilly, "<btilly at gmail.com>"
Thanks to Rent.com <http://www.rent.com> for their generous support in letting me develop and release this module. My thanks also to Keith Cascio "<cascio@helminthist.net>" for very helpful feedback on early drafts.
2019-03-01 | perl v5.28.1 |