Practical Example of Implementing OAuth 2.0 Using ory/hydra

When we building a server application that needs to protect some API resource, usually we only direct implemented simple authentication method. We send user’s credential directly to the server and then server respond with a session or returning a random string (access token) that we save in our database: stating that this {access_token} is owned by this {user_id}. Most often we produce JSON Web Token (JWT) that we don’t save in database, leading a session management issue because we cannot revoke specific JWT before it expired.

Above approach may fits in common use case, but it has pros and cons.

Pros:

  • The authentication and authorization process can be done in one handler/API, makes it easy to understand and implement.

Cons:

  • There is no separation of concern between authorization and authentication process.
  • Your user service may be blocked by tremendous data of access tokens.
  • Hard to maintain multi-application with same user’s credential. For example, service Gmail may consist of n services behind it and Youtube m services, but if we only build Auth service for Gmail only, it sometimes hard to Youtube to use the same login credential.

The latter cons may reminds you about “Sign In With Google” button that you can easily find when accessing web application that needs user account. That tiny button and flow behind it is powered by OAuth 2.0 authorization protocol. By implementing it into your app, you will be able to decouple authentication and authorization process. You can also create an Single Sign On (SSO) if you need the same user credential to be used across your service.

OAuth 2.0

Before we go, it will be useful to know what is OAuth itself. OAuth is not an authentication protocol, therefore it does not need to know user’s context. Although we can implement Open ID Connect as an authentication layer on top OAuth 2.0, it is a different topic that I will not explain here.

Authentication VS Authorization

Citing from Egor Homakov, he wrote:

authorize = permit 3rd party Client to access your Resources (/me, /statuses/new)
authenticate = prove that “this guy” is “that guy” which has account on the website.

In long words, we can read the analogy explanations from Okta:

From above explanation, there is word “permission” in the authorization process. In OAuth 2.0 itself, “permission”(s) are defined by key “scope” in the request payload when requesting grant access to Authorization server. Once granted, user will be given an access token to access a Protected Resource to proof that he or she is permitted to access it. For example, whether current user is able to access protected endpoint only for https://yourdomain.com/me or https://yourdomain.com/statuses/new or both endpoint.

Wait, it will be long explanation if I explain one by one. For now, I will list down it here:

  • Third Party Application (Client): or OAuth 2.0 Client is the external application (can be mobile app or web app or any application) that will access Protected Resource if Resource Owner (User) gives the access. To know that, the Client will ask Authorization Server.
  • Resource Owner (User): is a end-user who have username and password (or other credentials) to access Protected Resource.
  • Authorization Server: is a server to proof that this user is authorized or permitted to access the Protected Resource. In the last section we will use ory/hydra as Authorization Server.
  • Resource Server (Authentication Server): or others called it as an Identity Provider, is a server to proof that this user has the right credentials to do the operation in the Protected Resource. It commonly a username and password.
  • Protected Resource: or sometimes called as Resource Provider, is a server(s) where the data can only be accessed if user is authenticated AND has permission (authorized) to access that data.

For more information about the term, you can see:

You might think, why do we need this “permission”? Isn’t it clear, that if user already proof that it is him (authenticated using username and password), means that he can access all data inside protected resource (of course with WHERE user_id = :current_user_id)?. If you build a monolithic service, it is correct. But, if you still remember about what SSO that I briefly mentioned above, you may change your mind.

Imagine that you are now building a user-based services where your user credential is not only accessed by application that you make on your own (highly privileged application), but another application created by 3rd party may access user’s info. Hence, this 3rd party application must be given approval by the user.

For example, when you implement “Sign In With Google” in your app, you are not accessing email and password of the Google’s user. Instead, you only ask Google authorization service, “Hey Google, I have an application and wanna make an access and request on your user’s behalf.”, and then Google said, “Okay, please redirect my user to our authentication page and ask them what permissions they give to your application.” Then Google will validates whether email and password is correct. If correct, then it will ask their user what permission that our application can access. Once given, Google will give an access token to our application for further operation.

Those processes is happen because the application that will access Google Account data it not created by Google (highly privileged application from Google), but it is an application or service that you created. So, in order to protect their user’s Google Account credentials, Google need to make sure that their user only send their data to their system (in Google authentication page) and ask their user whether they permitted your application to access your data on your behalf (in Authorization page).

Now, lets the position is swapped: you are now creating something like Google Account service and you give the possibilities for 3rd party app uses your user account on their system.

This is what OAuth do. It gives you “an industry standard” about how authorization process will be made. By implementing this, you will save your development time because you don’t need to think about authorization flow because it already use by major company. In addition, with it vast adopter, your client (the ones who will use your user’s info) doesn’t need to learn your new proprietary logic because once they know OAuth, they will easily integrates in your system. Also, you don’t need to create your own library or SDK just for obtaining access token, because there is plenty OAuth Client library out there.

Grant Type: A Method To Authorize Your Client

OAuth 2.0 offers some grant type to authorize the 3rd party apps/clients to access to your data. The authorization process for mobile application or website will differs from smart-TV application or back-end’s service authorization process. This because each application runs in different capability limitation, and since OAuth 2.0 usually need web browser to redirect and run to authentication page process, some grant type is arranged to comply with device limitations.

In simple words: grant type is a set of flow about how the client (3rd party app) request the access token to access the protected resources.

OAuth 2.0 Grant Type: Authorization Code

TLDR; If you still don’t understand about my explanation below, may be this great article will easier to understand by you:

One of grant type that is commonly used is Authorization Code. The flow diagram is as follows:

Figure 1. Authorization Code Flow. Image source: https://darutk.medium.com/diagrams-and-movies-of-all-the-oauth-2-0-flows-194f3c3ade85 accessed at 6 February 2021 18.39 GMT+7

You may confused about term “Service ABC Resource Server” here, because if we refer to above terminology, the Resource Server is the API server used to access the user’s information, but in this image it seems like a Protected Resource. Yes, if we refer to above glossary, the “Service ABC Resource Server” itself actually is a Protected Resource.

In this diagram, the Resource Server and the Authorization Server (based on above glossary) is in the same server, called “Authorization Server”. So, the term Resource Server and Authorization Server can be used interchangeably if we see the diagram. But, for the clarity, I still call them as two different entity: Resource Server referring to a server who know about user’s identity, and Authorization Server referring to server to issues an access token and to check whether issued token is valid and has specific grant or not.

Step 1: Resource Owner (User) will click a button “Sign With {Your App Name}” in the Third Party Application (Client).

Step 2: Step 1 button will redirect page to Authorization Server to make authorization request. In OAuth 2.0, this request will be made using these query parameters:

  • client_id: A Client ID that the Third Party Application (Client) owner obtained from the OAuth 2.0 service. This includes client_secret.
  • redirect_uri: a Front-End or website page or URI where the access token (and sometimes refresh token) will given back. This URI must be registered first when creating an OAuth 2.0 Client. Usually it will be redirected to the Third Party Application (Client) page.
  • response_type: for Authorization Code grant, use “code”.
  • scope: list of permission that client (or the 3rd party) asks to access protected resource on user’s behalf.
  • state: a random code to be validated later when access_token is given back to the application. This is used to prevent CSRF attacks.

The final URI will be like this: http://your-authorization-server.com/oauth2/auth?client_id=myclient&redirect_uri=http%3A%2F%2Flocalhost%3A1234%2Fcallbacks&response_type=code&scope=users.write+users.read+users.edit+users.delete+offline&state=2nJR8aiorREJYUuePL29z0JucIo4guU7rx1qZLT4mTc%3D

Step 3, 4: Login page will be displayed to the user.

Step 5: The user then input their credentials that will be validated by Resource Server (Authentication Server). In this image, the process of Authentication may be done in the same server of Authorization Server. Actually, the processes in this step are:

  • Resource Server will validates the username and password.
  • Once Resource Server say “okay”, then Resource Server may request to Authorization Server to ask Resource Owner (User)’s consent about what permissions he/she give to Third Party Application (Client) to access. This process done by sending “scope” parameters to the Authorization Server.
  • When authentication and permission process is done, Resource Server then ask Authorization Server to issue short-live authorization code. This code will be used later by the Third Party Application (Client) to be exchanged with Access Token to access Protected Resource.

Step 6: A short-lived authorization code is given back to the Third Party Application (Client) by Authorization Server.

Step 7: Third Party Application (Client) then exchange the authorization code with the Access Token via Authorization Server by presenting the short-lived code itself and a client_secret.

Step 8: Authorization Server issues an Access Token.

Steps A-D shows a process in the Back-end called Service ABC when a Third Party Application (Client) requesting a protected data.

Step A: Third Party Application (Client) request a protected data, for example an user profile to Service ABC with an access token obtained from Step 1–8.

Step B: Service ABC then validate the access to Authorization Server.

Step C: Authorization Server then return the response, stating that “this access token is owned by this subject (can be user id or any unique identifier) and has these several permissions (via granted scope in the authorization process).”

Step D: Service ABC receives the access token’s subject and granted scope info. If accessing user profile is granted by the user (in the authorization process), then Service ABC must return the requested data, that is user info.

Implementing OAuth 2.0 using ory/hydra

The better way to learn is by practicing. Here I will show you how to implement Authorization Code grant flow in OAuth 2.0 using ory/hydra.

As you can read, Hydra is not an identity provider. This means that does not manage your users credential and profile. This allows you to implement user management and login your way, in your technology stack, with authentication mechanisms required by your use case (token-based 2FA, SMS 2FA, etc).

Let’s we begin to spin up Hydra server and our Resource Server (Identity Provider). Although the 5-minutes tutorial already explains how to run hydra using Identity Provider implemented in NodeJS in the ory/hydra-login-consent-node repository, but it will be easier to understand if we implement by ourself.

Before we go, we need to understand to integrates our Identity Provider to Hydra by looking at following sequence diagram:

Figure 2. OAuth 2.0 Flow using Ory/Hydra. Image source: https://www.ory.sh/hydra/docs/concepts/login accessed 7 February 2021 21.13 GMT+7

By looking at Figure 2, we know that we need to implements 4 (four) HTTP handlers (and I will also explains the logic of respective functions):

  1. Login Page — a Web UI that show a form for user to input their credentials. This is needed in process “Redirects end user with login challenge” and to “Fetches login info”. In this page, we need to capture a login_challenge from query parameter that sent by Hydra when “OAuth 2.0 Client initiates OAuth2 Authorize Code or Implicit Flow.”
    We then need to call Hydra to check whether current user already logged in (checked via cookie or remember flag). If already logged in (response key skip is true) then we do not need to show this login page, instead we use previous login info data to accept the login request and “Redirects end user to redirect url with login verifier”. Otherwise, we need to show a login form.
  2. Login Handler — an API to handle user’s info login. This handler will retrieve username and password of the User, then it will validate whether the inputted credentials is true. When valid, we need to accepts the login request to Hydra using login_challenge code sent by Hydra in process “Redirects end user with login challenge”. After accepting the request, Hydra will generate a consent_challenge and “Redirects end user with consent challenge” to Consent Page.
  3. Consent Page — a Web UI shows a list of permission that Resource Owner (User) need to accepts. For example, if the app need to access User’s profile info, it probably show a checkbox to access user info where the User can unchecked it (disagree) or checked it (agree). In this page, we will receive a consent_challenge that we need to send back to Hydra with list of scopes (permissions) that user agreed.
    We need to ask Hydra whether current request has requested the same scopes from the same user previously. If yes then skip parameter is true and you must not ask the user to grant the requested scopes.
  4. Consent Handler — an API handler to receive action of Consent Page (process 3). This handler will retrieve a grant_scope that User agreed in Consent Page UI, then ask Hydra to accepts Consent Request. The final response of this proccess is a redirect URI where Hydra will “Redirects to redirect url with consent verifier”. Hydra then verifies grant and transmits authorization code/token back to the redirect_uri in Third Party Application (Client). The Client then needs to exchange this Authorization code with an access token.

You may still cannot imagine what Login Page and Consent Page looks like. In “Sign In With Google” the Login Page will look like this:

Figure 3. Sample of Login Page in “Sign In With Google” button. Image source: https://developers.google.com/identity/sign-in/web/sign-in accessed at 8 February 2021 09:04 GMT+7
Figure 4. Consent Page in “Sign In With Google”. Image source: https://stackoverflow.com/questions/60954001/how-can-i-configure-google-oauth-consent-screen-to-not-show-checkboxes accessed at 8 February 2021 09:08 GMT+7

To implement those functions, I created a new Identity Provider based on Golang project and put those logic in authcdirectory. For each HTTP handlers above, I created these endpoint:

  1. GET /authentication/login — https://github.com/yusufsyaifudin/oauth2-example-hydra/blob/v0.1.0/cmd/authc/handler/login_get.go
  2. POST /authentication/login — https://github.com/yusufsyaifudin/oauth2-example-hydra/blob/v0.1.0/cmd/authc/handler/login_post.go
  3. GET /authentication/consent — https://github.com/yusufsyaifudin/oauth2-example-hydra/blob/v0.1.0/cmd/authc/handler/consent_get.go
  4. POST /authentication/consent — https://github.com/yusufsyaifudin/oauth2-example-hydra/blob/v0.1.0/cmd/authc/handler/consent_post.go

All of those endpoint is located in http://localhost:8000 so we need to configure Hydra:

URLS_LOGIN=http://localhost:8000/authentication/login
URLS_CONSENT=http://localhost:8000/authentication/consent

To know more about OAuth 2.0 Authorization Code Grant flow, we also need the Third Party Application (Client) to initiates OAuth 2.0 Authorization Code request. In the 5-minutes tutorial it use hydra token user command using below docker compose command:

docker-compose -f quickstart.yml exec hydra \
hydra token user \
--client-id auth-code-client \
--client-secret secret \
--endpoint http://127.0.0.1:4444/ \
--port 5555 \
--scope openid,offline

It actually run this code https://github.com/ory/hydra/blob/v1.9.2/cmd/token_user.go in port 5555.

Again, to make it more understandable, we need to create our own Front-End system that act as OAuth 2.0 Third Party Application (Client). We will create it using Golang with official library golang.org/x/oauth2 and run it in port 1234. You can see this code inside frontenddirectory. This application only consist of 2 endpoints:

Now, we can run Hydra using this docker compose:

version: '3.7'

services:
hydra-migrate:
image: oryd/hydra:v1.9.0
restart: on-failure
networks:
- ory-hydra-network
command:
migrate sql -e --yes
environment:
- DSN=postgres://hydra:secret@postgresd:5432/hydra?sslmode=disable&max_conns=20&max_idle_conns=4
depends_on:
- postgresd

hydra:
image: oryd/hydra:v1.9.0
restart: on-failure
networks:
- ory-hydra-network
ports:
- "4444:4444" # Public port
- "4445:4445" # Admin port
- "5555:5555" # Port for hydra token user, testing purpose only
command:
serve all --dangerous-force-http
environment:
# https://www.ory.sh/hydra/docs/reference/configuration
# https://github.com/ory/hydra/blob/aeecfe1c8f/test/e2e/docker-compose.yml
- SECRETS_SYSTEM=this-is-the-primary-secret
- URLS_LOGIN=http://localhost:8000/authentication/login # Sets the login endpoint of the User Login & Consent flow.
- URLS_CONSENT=http://localhost:8000/authentication/consent # Sets the consent endpoint of the User Login & Consent flow.

# set to Hydra public domain
- URLS_SELF_PUBLIC=http://localhost:4444 # to public endpoint
- URLS_SELF_ISSUER=http://localhost:4444 # to public endpoint
- DSN=postgres://hydra:secret@postgresd:5432/hydra?sslmode=disable&max_conns=20&max_idle_conns=4
- SERVE_PUBLIC_PORT=4444
- SERVE_PUBLIC_HOST=0.0.0.0
- SERVE_PUBLIC_CORS_ENABLED=true
- SERVE_ADMIN_PORT=4445
- LOG_LEVEL=debug
- TRACING_PROVIDER=jaeger
- TRACING_PROVIDERS_JAEGER_SAMPLING_SERVER_URL=http://jaeger:5778/sampling
- TRACING_PROVIDERS_JAEGER_LOCAL_AGENT_ADDRESS=jaeger:6831
- TRACING_PROVIDERS_JAEGER_SAMPLING_TYPE=const
- TRACING_PROVIDERS_JAEGER_SAMPLING_VALUE=1
depends_on:
- postgresd
- jaeger

postgresd:
image: postgres:13
restart: on-failure
networks:
- ory-hydra-network
ports:
- "5433:5432"
environment:
- POSTGRES_USER=hydra
- POSTGRES_PASSWORD=secret
- POSTGRES_DB=hydra
volumes:
- ./logs:/var/lib/postgresql/data

adminer:
image: adminer
restart: always
networks:
- ory-hydra-network
ports:
- 9000:8080

jaeger:
image: jaegertracing/all-in-one:1.7.0
restart: on-failure
networks:
- ory-hydra-network
ports:
- 5775:5775/udp
- 6831:6831/udp
- 6832:6832/udp
- 16686:16686 # Web App GUI to see traces

networks:
ory-hydra-network:
name: ory-hydra-net

Then you can run using docker-compose up in terminal. It will spin up a two HTTP server, one in port 4444 for Hydra Public Endpoint. This is an OAuth 2.0 server and should be publicly available by the Third Party Application (Client). Then port 4445 is for Hydra Admin and must not be available to public. You can deploy it behind a VPN where only your Identity Provider can access it. This Hydra Admin is used for accepting login request and consent request as well as revoking it.

We already set in URLS_LOGIN and URLS_CONSENT to http://localhost:8000 as our Identity Provider server, now we can try to run the pre-built binaries to serve Login and Page consent by downloading the binaries from https://github.com/yusufsyaifudin/oauth2-example-hydra/releases/tag/v0.1.0 or by git clone and run directly on your machine:

Clone the repository:

$ git clone git@github.com:yusufsyaifudin/oauth2-example-hydra.git
$ cd oauth-example-hydra
$ git checkout v0.1.0

Then download dependencies:

$ go mod download

Now run the Identity Provider:

$ go run cmd/authc/main.go

It will run a HTTP server in port 8000.

Before we can use the front-end application, we need to create an OAuth 2.0 Client App by requesting to Hydra Admin URL in different terminal tab:

curl -X POST 'http://localhost:4445/clients' \
-H 'Content-Type: application/json' \
--data-raw '{
"client_id": "myclient",
"client_name": "MyApp",
"client_secret": "mysecret",
"grant_types": ["authorization_code", "refresh_token"],
"redirect_uris": ["http://localhost:1234/callbacks"],
"response_types": ["code", "id_token"],
"scope": "offline users.write users.read users.edit users.delete",
"token_endpoint_auth_method": "client_secret_post"
}'

In above command, we create an OAuth 2.0 Client with client_id myclient and client_secret mysecret. This client is only for OAuth 2.0 grant type authorization_code and refresh_token. We also define a custom scope for this client, that is users.write, users.read, users.edit, and users.delete. The offline and openid scope is for Open ID Connect, see here https://www.ory.sh/hydra/docs/v1.8/debug#oauth-20-refresh-token-is-missing

Here, we also set the Front-End URL (or Mobile Apps URI) to http://localhost:1234/callbacks as a whitelist to retrieve authorization code.

Then, open a new terminal tab and run:

REDIRECT_URL=http://localhost:1234/callbacks CLIENT_ID=myclient CLIENT_SECRET=mysecret go run cmd/frontend/main.go
Figure 5. Left tab is for docker compose, top-right tab is for Identity Provider run in port 8000, bottom-right is for Front-End run in port 1234.

Now, open in private session web browser to http://localhost:1234, it should show following page:

Figure 6. Page show “Click here to _Sign In with YourApplicationName_”

If you cannot understand what the page says, it is in Indonesian and freely translated in English as “Click here to _Sign In with YourApplicationName_”. Then if we click that button, it will redirect to Login Page from Identity Provider authc that we build in above:

Figure 7. A Login Page in Identity Provider.

Look at the address bar, you see that Hydra send login_challenge payload in query parameter. Use username user@example.com and password password to authenicate the user with id 1. Then it will show the Consent Page with consent_challenge in query parameter:

Figure 8. A Consent Page in Identity Provider

In this example, we request all possible scope that myclient can access. So, it will show like Figure 8. But, in reality we can make offline and openid scope to always included in the request (when accepting the Consent Challenge).

Please also keep in mind that the scope it self is just a set of definition that your Identity Provider should be handle. Hydra as an Authorization Server only save it and does not do anything about it. For example, when we say users.write it does not tell Hydra to give user permission to write data. Instead, your Identity Provider (Resource Server) should define and also handle the name of each scopes. That’s why when you are implementing several OAuth 2.0 provider like Google or Github, they have their own scope definition.

Back to figure 8, when we click button “Authorize” it will then accepts the consent request in the Identity Provider (endpoint POST /authenticate/consent) and also redirect back to the front-end. Front-end then will retrieve the authorization code in the query parameter and exchange it with the real access token.

Figure 9. A Front-End callback retrieve a authorization code and state to be validated.

The process after retrieving token is up to you. You can save it in session or secure place in your application. Please note that this example is just for education purpose only, for deploying it into production you must be aware of several parameter. For example, in Mobile Apps application you must implements PKCE to add security layer in the authorization code exchange process.

References which also good to read

Yogyakarta, 8 February 2021. Work From Home.

Yogyakarta — Indonesia