DEV Community

jakubkoci
jakubkoci

Posted on

OpenID Connect Login

I've implemented OpenID Connect (OIDC) login a few times in my career. Two times this year, actually. But I always forget all the steps and details, so I wanted to write them down in this article for future reference. Even if we use a library to manage the majority of those steps, we we can still benefit knowing the details under the hood.

I'm going to describe Authorization Code flow with PKCE. This diagram illustrates the flow from a high-level perspective:

Terminology

The OpenID Connect is built upon the OAuth specification. The OAuth defines quite a specific terminology that we don't usually use in our day-to-day engineering work. There are a few terms from the The OAuth 2.1 Authorization Framework spec I intentionally simplified or omit for the purpose of this writing:

  • User Agent: the Browser, just a regular web browser
  • Client: the App, a confidential, web application client
  • Authorization Server: the Identity Provider as referred to in OIDC specification
  • Resoruce Server: The Identity Provider issues a JWT at the end of the process that is then used to access a Resource Server. That part is out of scope of this article.

For the purpose of this article, I also use the terms front-channel and back-channel that are commonly used, though not part of the OAuth specification:

  • Front-channel: Unsecure, browser-based redirects. We should not send any sensitive information through this channel.
  • Back-channel: Secure, direct server-to-server communication between the App and the Identity Provider.

Flow

I haven't found a better way to explain the flow than using HTTP requests and responses. It's independent of a particular programming language and can demonstrate lower-level details without too much boilerplate. The flow begins when a user enters the URL of the App into the Browser. The Browser sends a request to the App without a session cookie yet (step 1).

GET / HTTP/1.1 
Host: app.com
Cookie:
Enter fullscreen mode Exit fullscreen mode

The flow begins when a user enters the URL of the App into the Browser. The Browser sends a request to the App without a session cookie yet (step 1).

The App sends a response to the Browser with a redirect to the the Identity Provider (step 2). We can't send anything sensitive via a front-channel because it's publicly visible.

HTTP/1.1 302 Found
Location: https://identityprovider.com/login?response_type=code&client_id=acme&redirect_uri=https://app.com/api/auth/login-callback&scope=profile,email&state=xyz789random&code_challenge=xyz&code_challenge_method=S256
Enter fullscreen mode Exit fullscreen mode
  • response_type: Suggests we want to use the Authorization Code flow from the OAuth spec.
  • client_id: An identifier of the App.
  • redirect_uri: Where the Identity Provider should redirect the user after successful login.
  • state:
    • Protection against CSRF (Cross-Site Request Forgery) attacks.
    • Randomly generated string.
    • Must be verified by the App when handling the login-callback request as described in the next step.
  • code_challenge: PKCE part of the flow.
    • Protection against Authorization Code Injection/Replay attacks.
    • Generated based on a randomly generated code_verifier. The app adds only the code_challenge to the URL. The app will send the code_verifier to the Identity Provider later.

Side note: The state and code_verifier must be stored somewhere during the authorization flow. In our case, we use a web application with a server component so we can store both state and code_verifier temporarily in a server-side session storage.

The flow continues with the Identity Provider showing a login form. After successful login, the Identity Provider sends a response to the Browser with a redirect to the App (step 3). It uses redirect_uri as the redirect location. Again, this is a front-channel, so we can't just send the token yet.

HTTP/1.1 302 Found
Location: /api/auth/login-callback?code=123state=xyz789random 
Host: app.com
Enter fullscreen mode Exit fullscreen mode
  • code: Generated by the Identity Provider and stored together with code_challenge and redirect_uri to be verified later.
  • state: Received from the App in the previous step (step 2). The app checks if the state matches the value it sent before.

The App sends a token request to the Identity Provider (step 4). Although we put the values into URL params, we sent them in the body. That means we're in secure back-channel and we can finally send sensitive information such as client_secret and code_verifier.

POST /token HTTP/1.1
Host: identityprovider.com
Content-Type: application/x-www-form-urlencoded

code=123&client_id=acme&client_secret=acme-123&code_verifier=verified-xyz&redirect_uri=https://app.com/api/auth/login-callback
Enter fullscreen mode Exit fullscreen mode
  • code: Received from the Identity Provider in the previous step.
  • client_id: The same ID as in the first step.
  • client_secret: The secret obtained before, together with client_id, during the App registration (not part of this article).
  • redirect_uri:
    • Protects against Authorization Code Redirection URI Manipulation.
    • Must be the same value the App sent in the first request.
  • code_verifier: Retrieved from a storage based on the state.

The Identity Provider verifies that client_id and client_secret are correct. It checks if the code_verifier matches the previously stored code_challenge and if the redirect_uri matches the previously stored redirect_uri for the given code. Then, it sends a response with token(s) to the App (step 5).

HTTP/1.1 200 OK

{ 
    "access_token": "eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ"
    "refresh_token": "def50200a8f4b9c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5"
}
Enter fullscreen mode Exit fullscreen mode
  • access_token: A token used to access a Resource Server. It usually has short expiration period in minutes.
  • refresh_token: A token that has longer expiration period (half an hour and longer) and it's used to refresh the access_token when that expires.

The App sends a response to the Browser with redirect to home page at /. The response contains session as an http-only cookie.

HTTP/1.1 302 Found
Location: /
Set-Cookie: access_token=eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ; Path=/; HttpOnly; Secure; SameSite=Strict; Max-Age=900
Set-Cookie: refresh_token=def50200a8f4b9c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5; Path=/api/auth/refresh; HttpOnly; Secure; SameSite=Strict; Max-Age=604800
Enter fullscreen mode Exit fullscreen mode

All the following requests the Browser sends to the App, automatically contains the session.

GET /whatever HTTP/1.1
Host: app.com
Cookie: access_token=eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ
Enter fullscreen mode Exit fullscreen mode

If you want to understand more about the OAuth in general, this is the best talk on OAuth I've ever seen OAuth 2.0 and OpenID Connect (in plain English).

Top comments (0)