Let’s assume you as the developer of your custom app want to integrate a feature that allows a user to import their Google Contacts from within your app. How can you do that? How does authentication (who you are) and authorization (what you are allowed to do) work?
From a security point of view it would be really bad if you had to store the user’s Google username and password within the app. As you will see, thankfully most of the security related stuff is not handled by your app but from the third-party platform that is providing the resource that you want to access – in our example Google.
Here is what you do: The goal is to get an access token from a Google OAuth2 server that your app (client) can use to access Google contacts (resource server) on behalf of the user (resource owner). That access token only grants temporary access and can be revoked anytime using the Google user interface (note: not in your app). In order to get that token your app redirects you to the Google login page on which you have to enter your Google username and password. The username and password is never leaked to your application. After that the user has to choose (in Google) whether to allow your app access to Google Contacts.
To summarize the main players in OAuth2:
- Person (resource owner) who wants to have access to a certain resource
- A client application. There are different auth flows depending on whether the client application is a server rendered application or a Single Page Application (SPA) or respectively a Native App
- A Resource server that contains the protected resource
- An OAuth2 server (Auth Server)
OAuth2’s primary purpose is authorization and not authentication. Many companies abused OAuth2 to build their own proprietary authentication mechanism that introduced severe security flaws in which malicious apps could impersonate authenticated users. OpenID Connect is a standardized way of authenticating users, it is build on top of OAuth2.
You have to choose between so called OAuth2 Flows which are variations of slightly different implementations for your app. Which Flow you choose depends on the type of your application:
- Code flow is used if you try to authorize using
response_type=code
. This is used to get an auth code. You want to use this approach when you have a web application that is rendered on the server - Implicit flow is used if you try to authorize using
response_type=token
. This is actually a simplified way of getting the access token and should be used for Single Page Applications or Native Apps. - Resource Owner Credentials Flow is used if you try to authorize using
grant_type=password
. This is used if you trust your custom app to store username and password within your app.
Step 0: Initial setup
There is an initial setup step required. Your client needs to register with the Auth Server. That can be done dynamically during runtime or ahead of time by using a client id and a password that you can use later on to authenticate against the Auth server. The credentials should be stored securely in a credential store like Vault.
Step 1: Sending auth request
A common authorization scenario looks like this: In your web client you browse to /resource
which requires authorization. You don’t want to give the client your password, instead you want to retrieve a OAuth2 access token from the authorization server. We call this the “Authorization request flow” and it looks like this:
Upon browsing to /resource
the browser is instructed by the application server to be redirected to the Auth Server:
GET /authorize? client_id=webapp& scope=resource& redirect_uri=https://webapp/cb& response_type=code& state=123
The request above basically says: I am the client named “webapp
” and I need to access “resource
“. Please check if I am authorized and redirect me afterwards to “https://webapp/cb
“. Also I asked you to send me an auth code via response_type=code
, please create it and send it to me. By the way: I would have requested response_type=token
if I was a Single Page Application or native app instead of a server rendered web app. I also send you my state "123"
that I stored locally and that you need to send back to me, so I can make sure that your answer matches my request (to prevent CSRF).
Step 2: Authenticate to Auth Server
On Auth Server: If the resource owner is not already authenticated, the Auth Server responds with displaying the Auth Server’s login page on which the user must provide login credentials and choose whether to allow or deny that “webapp” can access information.
In this step, the server response differs depending on whether we have a web application that is rendered on the server or if we have a Single Page Application.
In case of a Single Page or Native Application the Auth Server sends:
GET /cb#access_token=abc& expires_in=2600& state=123
That means the access token is exposed to the client. In case of a server rendered web application the Auth Server sends us only a auth code and not the access token:
GET /cb?code=xyz&state=123
Note that we use a #
in SPAs and a ?
in server rendered apps. A hash sign prevents sending any parameters and thus protects leaking the access token to any potential third party. Also in contrast to a server rendered web application we get the access token right away instead of having to request it – as we will do now in Step 3.
Step 3: Requesting access token
This step can be skipped for Single Page Applications as mentioned before: we already have an access token. But for server rendered web applications we do this:
POST /token Authorization: Basic (client_id: secret) grant_type=authorization_code& authorization_code=xyz& redirect_uri=https://webapp/cb
Client server says to Auth Server: Here, take my stored client_id
and secret
that I got from you when I registered myself to you earlier. Also, here is the auth code xyz
that I got from you. After you are done checking me please redirect me to https://webapp/cb
.
Step 4: Accessing resource using access token
Again, if we had a SPA then we already got our access token in a previous step. But for server rendered apps the Auth Server checks the clients’ id, secret and auth code. If the check turns out to be valid then the Auth Server sends the following to the client:
{ "access_token" : "abc", "expires_in" : "3600", "token_type" : "Bearer", "refresh_token" : "xyz" }
Here Auth Server says to client: You can use that access token
for 3600
seconds to access the resource. If you need more time, then you can request another access token by using the refresh token xyz
.
The generated access token usually is a JSON Web Token (JWT) and contains resource owner id, client id, expiration date, granted scopes (specific read/write info) and any additional info that make sense for the application.
The client can now access the Resource Server by sending the access token:
GET /resource Authorization: Bearer access_token
Refresh tokens are only available for server rendered applications but not for SPAs. If the client wants to refresh the token:
POST /token Authorization: Basic (client_id: secret) grant_type=refresh_token& refresh_token=xyz
The resource owner can also revoke the clients’ access any time, usually by logging into the Auth Server and removing the client permission there. Technically this means that the Auth Server does not provide a refresh token to the client anymore.