DEV Community

Cover image for Demystifying OAuth 2.0 - A Tutorial & Primer
Devan
Devan

Posted on • Updated on • Originally published at devansvd.com

Demystifying OAuth 2.0 - A Tutorial & Primer

OAuth - Oh! Auth. It may be one of the confusing topics to grasp a clear understanding.

The Confusion begins in the word itself O*Auth* , perhaps it means Authentication (or) Authorization ?

This primer is written for me to understand clear OAuth concepts & aimed to provide answers to questions like

  • Why OAuth exists ?
  • Can I use OAuth for Authentication ?
  • Why the trend social login - Login with google, etc. ?
  • How to use OAuth and it's different types.

This article was originally published on https://devansvd.com/oauth. Visit if you're interested.

Let's get started !


Problem OAuth Solves

Imagine the world without OAuth, Let's take a real-world example, say a third-party application like printing service needs to access your photo's stored in your google drive to print.

For providing this access you (or) google needs to share your google login credentials (email/password) with the third-party application. So they can access your photos and print. Yes that's Right - Your google password.

Nobody wants this Right ? They eventually not only have access to your google drive, they can impersonate as you, read your emails, contacts, even credit card/passwords stored basically they become you. OAuth specifically designed to solve this problem.

Image of world without OAuth

Believe me, this is how it literally worked in the dark stone ages before OAuth. For Example: Yelp.com is asking for your google credentials to access your contacts stored in google and send invites to your friends.

See for yourself - "Don't worry, we don't keep your email password or your friends addresses". Sounds like a Fake Promise to me.

Image of world without OAuth

What is OAuth ?

OAuth 2.0 is an Authorization framework, designed specifically where a user of a service can allow a third-party application to get limited access to his/her data hosted in the service without revealing his/her username & password credentials to the third-party application.

It enables limited access to the third-party on user's data with their consent, Sort of usually like accessing on behalf of the user is often called Delegated Access.

The OAuth 2.0 - RFC 6749 explicitly mentioned it is an Authorization framework. So should not be used for Authentication purposes. Although due it's convenience, many people started using it for Authentication. We will discuss why it shouldn't be used for Authentication in detail, for now just know it should not to.

Authentication vs Authorization

To understand OAuth, One needs to know the difference between Authentication and Authorization.

  • Authentication = login + password (who you are)
  • Authorization = permissions (what you are allowed to do, this is where OAuth comes in)

How OAuth Solves the Problem ?

OAuth provides an Authorization mechanism to fine grained control of 'What you are allowed to do' in a protected http resource. The best part is, it can be Revoked anytime.

The OAuth framework specifies several grant types for different use cases. The most common OAuth grant types are listed below.

Also, It provides an extensibility mechanism for defining additional types as well means it can be Extended as per needs.

Note: After few years of usage somebody found Authorization code grant is vulnerable to authorization code interception attack when using with public client type.

To overcome this RFC 7636 - Authorization Code Grant with PKCE (Proof Key for Code Exchange by OAuth Public Clients) pronounced "pixy" is introduced, for now just know pixy exists.

Some Technical OAuth terms

Before getting started, Need to familiar with few technical OAuth terms you'll often hear. When using OAuth Mostly four Parties or Roles involved.

  • Resource owner (End user)
  • Resource server (Google drive)
  • Client (Third party application - Printing service)
  • Authorization server (Google Authentication server)

Basic Protocol flow

The abstract of the OAuth protocol is as follows, this way it solves the mentioned problem.

Image of OAuth Abstract flow

For the Client (third-party) to access a protected resource (google drive photo),

  1. First, the client requests authorization from the resource owner (end user),
  2. Let's say the resource owner (end user) approves the authorization request the client receives authorization grant depends upon the grant type method used by the client (third-party).
  3. Then, the client (third-party) request's access token by presenting the received authorization grant to the Authorization server (Google Authentication server)
  4. Authorization server (Google Authentication server) validates client and grant issues access token to the client (third-party)
  5. Client (Third party) uses the access token to gain access to the protected resource in the resource server (google drive).

Refer each grant type flow for how exactly each flow works.

How to use

To use OAuth first one needs to register a client (Third party service) with the OAuth server by providing Client name, Client type, Intention of service and usage, redirect url, etc.

On successful client registration, two things provided to you by the OAuth server.

  1. Client Id - Can be exposed publicly.
  2. Client Secret - As the name implies, Must be kept secret & safe.

Client Types

When registering Client (Third party service) it is required to specify what type of client it is Public (or) Confidential.

  • Confidential (Server based with restricted access)
  • Public (on End user - brower based, native app)

Note: If it is a confidential client - Both Client Id & Client secret is mandatory must pass this in the request. If public client - Client id is sufficient to pass in the request.

It's enough with the technical terms, Let's see how each grant types one works in detail, Don't want to bore you with theoretical explanations, appropriate flow diagrams added with real world example, Refer the images.

Authorization Code Grant

  • Redirection based flow needs interaction with the resource owner user-agent (Typically browser or native apps).
  • Both access token and refresh token are supported

A client application makes an authorization request to the authorization server's authorization endpoint, On successful approval from the resource owner receives a short-lived authorization code in the url. Then, client request access token by presenting authorization code to the token endpoint of authorization server. Client receives access token which then used to gain access to the protected resource in the resource server.

Image of Authorization code grant

Request to Authorization Endpoint

GET - /authorize {Authorization Endpoint}
  ?response_type=code             // - Required
  &client_id={Client ID}          // - Required
  &redirect_uri={Redirect URI}    // - Conditionally required
  &scope={Scopes}                 // - Optional
  &state={Arbitrary String}       // - Recommended to prevent csrf
  HTTP/1.1
HOST: {Authorization Server}

Success Response from Authorization Endpoint

HTTP/1.1 302 Found
Location: {Redirect URI}
  ?code={Authorization Code}  // - Always included usually expires in 10 mins, Single time use
  &state={Arbitrary String}   // - Included if the authorization request included 'state'.

Error Response from Authorization Endpoint

HTTP/1.1 302 Found
Location: {Redirect URI}
  ?error={Error}          // Included
  &error_description={Error Description} // Optional
  &state={Arbitrary String}   // - Included if the authorization request included 'state'.

Error can be any one of the following

invalid_request
unauthorized_client
access_denied
unsupported_response_type
invalid_scope
server_error
temporarily_unavailable

Request To Token Endpoint

POST - /token {Token Endpoint} HTTP/1.1
Host: {Authorization Server}
Content-Type: application/x-www-form-urlencoded
Authorization: Bearer {Base64 encoded client_id:client_secret} // Required if confidential client
grant_type=authorization_code  // - Required
&code={Authorization Code}     // - Required
&redirect_uri={Redirect URI}   // - Required if the authorization request included 'redirect_uri'.
&client_id={Client Id}       // - Required both for public and confidential clients
&client_secret={Client Secret} // - Required for confidential client

Note: If client type is "Public" - client_id is required in the request body. If client type is "Confidential" - client_id and client_secret required as Authorization header, base64 encoded pair of client_id:client_secret (or) may be passed in the request body depending on the OAuth server.

It is recommended to use client_id and client_secret in the Authorization header instead of request body.

Response From Token Endpoint

HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Cache-Control: no-store
Pragma: no-cache
{
  "access_token": "{Access Token}",    // - Always included
  "token_type": "{Token Type}",        // - Always included
  "expires_in": {Lifetime In Seconds}, // - Optional
  "refresh_token": "{Refresh Token}",  // - Optional
  "scope": "{Scopes}"                  // - Mandatory if granted scopes differ from the requested ones.
}

Example for Authorization code grant

Visit working example for Authorization code grant flow with Keycloak OAuth Server : Open Authorization Code Grant Example

Authorization code grant with PKCE

This flow is an extension to Authorization grant flow. After few years of usage somebody found Authorization code grant is vulnerable to authorization code interception attack when using with public client type since public clients don't have a client secret.

To overcome this RFC 7636 - Authorization Code Grant with PKCE (Proof Key for Code Exchange by OAuth Public Clients) pronounced "pixy" is introduced.

It is recommended that all clients use the PKCE extension with this flow as well to provide better security. For detailed explanation on how this attack works view this Youtube video.

Image of Authorization code grant attack

PKCE for the rescue

  1. Your app needs to generate a code_verifier (string of 43 - 128 characters)
    • Remember this as part of the session needs to store somewhere.
  2. Use that code_verifier to generate a code_challenge
    • code_challenge = SHA256(code_verifier) //S256
    • code_challenge = code_verifier //PLAIN, not recommended
  3. Include code_challenge and code_challenge_method in authorization request
    • GET /authorize?client_id=...&scope=...&response_type=code&redirect_uri=customURL &code_challenge=base64url(code_challenge)&code_challenge_method=S256
  4. Include code_verifier in the token exchange request:
    • POST /token client_id=...&redirect_uri=...&code=acode&code_verifier=verifier

Image of Authorization code grant with PKCE

Request to Authorization Endpoint

GET - /authorize {Authorization Endpoint}
  ?response_type=code             // - Required
  &client_id={Client ID}          // - Required
  &redirect_uri={Redirect URI}    // - Conditionally required
  &scope={Scopes}                 // - Optional
  &state={Arbitrary String}       // - Recommended to prevent csrf
  &code_challenge={Challenge}     // - PKCE Protection
  &code_challenge_method={Method} // - PKCE Protection - S256 or PLAIN
  HTTP/1.1
HOST: {Authorization Server}

Success Response from Authorization Endpoint

HTTP/1.1 302 Found
Location: {Redirect URI}
  ?code={Authorization Code}  // - Always included usually expires in 10 mins, Single time use
  &state={Arbitrary String}   // - Included if the authorization request included 'state'.

Error Response from Authorization Endpoint

HTTP/1.1 302 Found
Location: {Redirect URI}
  ?error={Error}          // Included
  &error_description={Error Description} // Optional
  &state={Arbitrary String}   // - Included if the authorization request included 'state'.

Error can be any one of the following

invalid_request
unauthorized_client
access_denied
unsupported_response_type
invalid_scope
server_error
temporarily_unavailable

Request To Token Endpoint

POST - /token {Token Endpoint} HTTP/1.1
Host: {Authorization Server}
Content-Type: application/x-www-form-urlencoded
Authorization: Bearer {Base64 encoded client_id:client_secret} // Required if confidential client
grant_type=authorization_code  // - Required
&code={Authorization Code}     // - Required
&redirect_uri={Redirect URI}   // - Required if the authorization request included 'redirect_uri'.
&code_verifier={Verifier}      // - Required if the authorization request included 'code_challenge'.
&client_id={Client Id}       // - Required both for public and confidential clients
&client_secret={Client Secret} // - Required for confidential client

Note: If client type is "Public" - client_id is required in the request body. If client type is "Confidential" - client_id and client_secret required as Authorization header, base64 encoded pair of client_id:client_secret (or) may be passed in the request body depending on the OAuth server.

It is recommended to use client_id and client_secret in the Authorization header instead of request body.

Response From Token Endpoint

HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Cache-Control: no-store
Pragma: no-cache
{
  "access_token": "{Access Token}",    // - Always included
  "token_type": "{Token Type}",        // - Always included
  "expires_in": {Lifetime In Seconds}, // - Optional
  "refresh_token": "{Refresh Token}",  // - Optional
  "scope": "{Scopes}"                  // - Mandatory if granted scopes differ from the requested ones.
}

Example for Authorization code grant with PKCE

Visit working example for Authorization code grant flow with PKCE with Keycloak OAuth Server : Open Authorization Code Grant with PKCE Example

Implicit Grant

  • Access token will be received directly as a Url fragment (https://domain.com#access_token)
  • No intermediate steps like authorization grant flow
  • These clients are typically implemented in a browser using a scripting language such as JavaScript.
  • It is not recommended to use the implicit flow (and some servers prohibit this flow entirely) due to the inherent risks of returning access tokens in an HTTP redirect without any confirmation that it has been received by the client.

A client application makes an authorization request to the authorization endpoint of authorization server, On successful approval from the resource owner receives an access token directly in the url as a URL fragment. Client uses access token to gain access to the protected resource in the resource server.

Image of Implict grant

The Implicit flow was a simplified OAuth flow previously recommended for native apps and JavaScript apps where the access token was returned immediately without an extra authorization code exchange step.

Request to Authorization Endpoint

GET - /authorize {Authorization Endpoint}
  ?response_type=token            // - Required
  &client_id={Client ID}          // - Required
  &redirect_uri={Redirect URI}    // - Conditionally required
  &scope={Scopes}                 // - Optional
  &state={Arbitrary String}       // - Recommended to prevent csrf
  HTTP/1.1
HOST: {Authorization Server}

Success Response from Authorization Endpoint

HTTP/1.1 302 Found
Location: {Redirect URI}
  #access_token={Access Token}       // - Always included
  &token_type={Token Type}           // - Always included
  &expires_in={Lifetime In Seconds}  // - Optional
  &state={Arbitrary String}          // - Included if the request included 'state'.
  &scope={Scopes}                    // - Mandatory if granted scopes differ from the requested ones.

Error Response from Authorization Endpoint

HTTP/1.1 302 Found
Location: {Redirect URI}
  ?error={Error}          // Included
  &error_description={Error Description} // Optional
  &state={Arbitrary String}   // - Included if the authorization request included 'state'.

Error can be any one of the following

invalid_request
unauthorized_client
access_denied
unsupported_response_type
invalid_scope
server_error
temporarily_unavailable

Example for Implicit grant

Visit working example for Implicit grant flow with Keycloak OAuth Server : Open Implicit grant flow Example

Resource Owner Password Credentials Grant

  • Only use it on a trust relationship with the client such as the device operating system or a highly privileged application
  • The authorization server should take special care when enabling this grant type and only allow it when other flows are not viable.
  • It is not recommended that this grant be used at all anymore.

Resource owner provides username + password to the client, the client requests access token from the authorization server's token endpoint by including username, password, client_id & client_secret. The authorization server provides access token after validating the request. Client uses this access token to gain access to the protected resource.

Image of Password grant

Request To Token Endpoint

POST - /token {Token Endpoint} HTTP/1.1
Host: {Authorization Server}
Content-Type: application/x-www-form-urlencoded
Authorization: Bearer {Base64 encoded client_id:client_secret} // Required if confidential client
grant_type=password        // - Required
&username={Username}         // - Required
&password={password}         // - Required
&client_id={Client Id}       // - Required both for public and confidential clients
&client_secret={Client Secret} // - Required for confidential client

Note: If client type is "Public" - client_id is required in the request body. If client type is "Confidential" - client_id and client_secret required as Authorization header, base64 encoded pair of client_id:client_secret (or) may be passed in the request body depending on the OAuth server.

It is recommended to use client_id and client_secret in the Authorization header instead of request body.

Response From Token Endpoint

HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Cache-Control: no-store
Pragma: no-cache
{
  "access_token": "{Access Token}",    // - Always included
  "token_type": "{Token Type}",        // - Always included
  "expires_in": {Lifetime In Seconds}, // - Optional
  "refresh_token": "{Refresh Token}",  // - Optional
  "scope": "{Scopes}"                  // - Mandatory if granted scopes differ from the requested ones.
}

Example for Resource Owner Password Credentials Grant

Visit working example for Resource Owner Password Credentials Grant flow with Keycloak OAuth Server : Open Resource Owner Password Credentials Grant Example

Client Credentials grant

  • Often called as Machine to Machine Flow, Used only by servers.
  • Must only be used by confidential clients
  • No resource owner interactions in this flow.
  • The specification says Client Credentials Flow should not issue refresh tokens.

The client requests an access token from the authorization server's token endpoint by presenting client_id and client_secret. If valid authorization server issues access token to the client, then it uses this access token to gain access to the protected resource in the resource server.

Image of Client Credentials grant

Request To Token Endpoint

POST - /token {Token Endpoint} HTTP/1.1
Host: {Authorization Server}
Content-Type: application/x-www-form-urlencoded
Authorization: Bearer {Base64 encoded client_id:client_secret} // Required if confidential client
grant_type=client_credentials  // - Required
&client_id={Client Id}       // - Required both for public and confidential clients
&client_secret={Client Secret} // - Required for confidential client

Note: If client type is "Public" - client_id is required in the request body. If client type is "Confidential" - client_id and client_secret required as Authorization header, base64 encoded pair of client_id:client_secret (or) may be passed in the request body depending on the OAuth server.

It is recommended to use client_id and client_secret in the Authorization header instead of request body.

Response From Token Endpoint

HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Cache-Control: no-store
Pragma: no-cache
{
  "access_token": "{Access Token}",    // - Always included
  "token_type": "{Token Type}",        // - Always included
  "expires_in": {Lifetime In Seconds}, // - Optional
  "scope": "{Scopes}"                  // - Mandatory if granted scopes differ from the requested ones.
}

Example for Client Credentials Grant

Visit working example for Client Credentials grant flow with Keycloak OAuth Server : Open Client Credentials grant flow Example

Refresh token Grant

  • It is used to refresh the expired access token

When access token possessed by the client expires, client presents refresh token to the authorization server's token endpoint and request's new access token, then it uses this access token to gain access to the protected resource in the resource server.

Image of Refresh token grant

Request To Token Endpoint

POST - /token {Token Endpoint} HTTP/1.1
Host: {Authorization Server}
Content-Type: application/x-www-form-urlencoded
Authorization: Bearer {Base64 encoded client_id:client_secret} // Required if confidential client
grant_type=refresh_token        // - Required
&refresh_token={Refresh Token}  // - Required
&client_id={Client Id}       // - Required both for public and confidential clients
&client_secret={Client Secret} // - Required for confidential client

Note: If client type is "Public" - client_id is required in the request body. If client type is "Confidential" - client_id and client_secret required as Authorization header, base64 encoded pair of client_id:client_secret (or) may be passed in the request body depending on the OAuth server.

It is recommended to use client_id and client_secret in the Authorization header instead of request body.

Response From Token Endpoint

HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Cache-Control: no-store
Pragma: no-cache
{
  "access_token": "{Access Token}",    // - Always included
  "token_type": "{Token Type}",        // - Always included
  "expires_in": {Lifetime In Seconds}, // - Optional
  "refresh_token": "{Refresh Token}",  // - Optional
  "scope": "{Scopes}"                  // - Mandatory if granted scopes differ from the requested ones.
}

Example for Refresh Token Grant

Visit working example for Refresh Token Grant flow with Keycloak OAuth Server : Open Refresh Token Grant flow Example

Other grant type exists ?

Yes, OAuth grant types are extensible by design.

Example: The Device Code grant type - urn:ietf:params:oauth:grant-type:device_code.

Device code grant

Consider, A Television needs to display photos from your google drive, It only has remote, Not possible to enter inputs to login, In this case can't use any other flows. So, the OAuth is Extended to Device code grant to provide support for these kinds of mechanism. Device Code grant type - urn:ietf:params:oauth:grant-type:device_code.

These device's are called input constraint devices. Example: TV, Printer, Speaker, etc.

A Televison sends a Device request to the Authorization server's device endpoint, Authorization server returns unique device_code, user_code, verification_uri, interval and expires_in. User needs to visit the Verification URL provided by the authorization server in any of their devices like phone, laptop, tablets, etc. and enter the user_code, then login with their credentials and approve. In the meantime, the Television application continously polling the token endpoint at the interval specified by authorization server. It will see authorization_pending error often. Once the user successfully loggedin the authorization server's token endpoint return the access_token and refresh_token to the televison application. As usual, using the access_token television application access your google drive and display's photo's.

Image of device code grant

Request To Device Endpoint

POST - /device {Device Endpoint} HTTP/1.1
Host: {Authorization Server}
Content-Type: application/x-www-form-urlencoded
Authorization: Bearer {Base64 encoded client_id:client_secret} // Required if confidential client
&client_id={Client Id}       // - Required both for public and confidential clients
&client_secret={Client Secret} // - Required for confidential client

Note: If client type is "Public" - client_id is required in the request body. If client type is "Confidential" - client_id and client_secret required as Authorization header, base64 encoded pair of client_id:client_secret (or) may be passed in the request body depending on the OAuth server.

It is recommended to use client_id and client_secret in the Authorization header instead of request body.

Response From Device Endpoint

HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Cache-Control: no-store
Pragma: no-cache
{
  "device_code": "{Device code}",     // - Always included
  "user_code": "{User code}",       // - Always included
  "verification_uri": "{Verification URL}",  // - Always included
  "interval": 5,          // - Always included
  "expires_in": 1800        // - Always included
}

Polling To Token Endpoint

POST - /token {Token Endpoint} HTTP/1.1
Host: {Authorization Server}
Content-Type: application/x-www-form-urlencoded
Authorization: Bearer {Base64 encoded client_id:client_secret} // Required if confidential client
grant_type=urn:ietf:params:oauth:grant-type:device_code // - Required
&client_id={Client Id}       // - Required both for public and confidential clients
&client_secret={Client Secret} // - Required for confidential client

Note: If client type is "Public" - client_id is required in the request body. If client type is "Confidential" - client_id and client_secret required as Authorization header, base64 encoded pair of client_id:client_secret (or) may be passed in the request body depending on the OAuth server.

It is recommended to use client_id and client_secret in the Authorization header instead of request body.

Response From Token Endpoint

HTTP/1.1 400 Bad Request
{
  "error": "authorization_pending"
}

Response From Token Endpoint after User loggedIn

HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Cache-Control: no-store
Pragma: no-cache
{
  "access_token": "{Access Token}",    // - Always included
  "token_type": "{Token Type}",        // - Always included
  "expires_in": {Lifetime In Seconds}, // - Optional
  "scope": "{Scopes}"                  // - Mandatory if granted scopes differ from the requested ones.
}

Visit this URL to how google device code api used to sign to the television.

Playground

To quicky play with All OAuth grant types visit OAuth Playground - https://www.oauth.com/playground

Problem with OAuth - directly used as an authentication protocol

The issued access_token doesn't have the information to whom the token is issued to i.e intended audience and it doesn't have a mechanism whether intended audience properly received it - It sort of like throwing access_token or authorization code Over the wall.

To store an Information about who logged in i.e.UserId Needed an additional api call which increases network latency. Some used secondary token (id_token) or JWT with audience possessed client information like UserId. Although OAuth itself doesn't define anything about this.

However, some people began to use OAuth 2.0 for authentication (not for authorization) and OAuth authentication has prevailed rapidly.

From a viewpoint of OpenID guys, authentication based on OAuth was not secure enough, but they had to admit that people preferred OAuth authentication. As a result, OpenID guys decided to define a new specification - OpenID Connect, on top of OAuth 2.0.

Yes, this has made people much more confused.

For more pitfalls on why OAuth shouldn't be used for Authentication head over to oauth.net common pitfalls.

As per suggestion - it is recommended to use OAuth only for Authorization purposes, not for authentications. For Authentication it is preferred to use "OpenID Connect" which is specifically built for this purpose.

Why the trend - login with social accounts ?

Social login is often implemented using the OAuth standard. Social login providers saw an Opportunity, they provided a solution with eliminates the need for users to remember login credentials on every website they visit. Since users already have an account with provides like google, facebook, etc - just use login with their option (Login with google).

Advantages:

  • Eliminates need to remember login credentials for every website visited.
  • Pre-validated Email address, opportunity to reduce fake user accounts.
  • Speed up the registration process - Information about users can be quickly obtained from profile data returned by social login, instead of manually entering.

Disadvantages:

  • May unintentionally render third-party websites useless within certain libraries, schools, or workplaces which block social networking services for productivity reasons.
  • These logins are also a new frontier for fraud and account abuse as attackers use sophisticated means to hack these authentication mechanisms.

I guess I could say this is an experiment with OAuth.

Feel free to share your thoughts and feedback.

I'm Devan - Aka devansvd.

References

Top comments (8)

Collapse
 
kattak2k profile image
Ravi Katta

That's a good article, I've recently implemented authorization code and client credentials, But nowhere I found the OAuth is secure but just to make users lazy to remember credentials.

Collapse
 
devansvd profile image
Devan

Be sure to use authorization code with pkce grant type.

Collapse
 
kattak2k profile image
Ravi Katta

I wish I could, Unfortunately, we had a client system which was built to accept only these 2 types.

Collapse
 
jankapunkt profile image
Jan Küster

Thank you for the diagrams. I think they make this whole topic way more accessible than just reading the plain text in the RFCs.

Collapse
 
devansvd profile image
Devan

Sure they do.

Collapse
 
danielkun profile image
Daniel Albuschat • Edited

Great article, Devan! I'm sure it will be helpful to many other devs or admins. I surely learned a thing or two. 👍

Collapse
 
devansvd profile image
Devan

Thanks. Means a lot to me.

Collapse
 
cdian profile image
cdian

The pkce challenge is also stored in a public client. This is also insecure like the implicit flow.