Spring OAuth 2
This post is a quick analysis of the Spring implementation of OAuth 2.0 code flow (RFC6749 Section 4.1) with the minumum application code. The setup consists of the authorization server, the resource server, and the client.
The flow starts when a user hits localhost:9999/client/resource
GET /client/resource HTTP/1.1
Host: localhost:9999
Client endpoints are protected by Spring Security, therefore it redirects the
request to login
endpoint
HTTP/1.1 302
Set-Cookie: JSESSIONID=67CB7BD60760EDD84BD0884FF9F09651;path=/client;HttpOnly
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Location: http://localhost:9999/client/login
Content-Length: 0
Browser tries to load login
page
GET /client/login HTTP/1.1
Host: localhost:9999
Cookie: JSESSIONID=67CB7BD60760EDD84BD0884FF9F09651
Client delegates the authorization to OAuth server
HTTP/1.1 302
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Location: http://localhost:8080/oauth/authorize?client_id=my-trusted-client&redirect_uri=http://localhost:9999/client/login&response_type=code&state=JUkFF6
Content-Length: 0
OAuth flow technically starts here. state
is randomly
generated by Spring. client_id
is configured in Client’s application
properties.
GET /oauth/authorize?client_id=my-trusted-client&redirect_uri=http://localhost:9999/client/login&response_type=code&state=JUkFF6 HTTP/1.1
Host: localhost:8080
Since User is not authenticated yet Spring shows basic dialog asking for user name and password.
HTTP/1.1 401
WWW-Authenticate: Basic realm="Spring"
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Strict-Transport-Security: max-age=31536000 ; includeSubDomains
Content-Type: text/html;charset=UTF-8
Content-Language: en-US
Content-Length: 344
User types dave
:secret
. These are credentials stored in Server database.
Authorization
header here contains User credentials.
GET /oauth/authorize?client_id=my-trusted-client&redirect_uri=http://localhost:9999/client/login&response_type=code&state=JUkFF6 HTTP/1.1
Host: localhost:8080
Authorization: Basic ZGF2ZTpzZWNyZXQ=
Server verifies User credentials and creates session.
HTTP/1.1 200
Set-Cookie: JSESSIONID=0517E6F4E263721C8E75A1378D51179F;path=/;HttpOnly
Cache-Control: no-store
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
X-Frame-Options: DENY
Strict-Transport-Security: max-age=31536000 ; includeSubDomains
Content-Type: text/html;charset=UTF-8
Content-Language: en-US
Content-Length: 573
The authorization page gives User a choice to authorize or deny Client.
User clicks Authorize
button.
POST /oauth/authorize HTTP/1.1
Host: localhost:8080
Content-Length: 44
Authorization: Basic ZGF2ZTpzZWNyZXQ=
Origin: http://localhost:8080
Content-Type: application/x-www-form-urlencoded
Referer: http://localhost:8080/oauth/authorize?client_id=my-trusted-client&redirect_uri=http://localhost:9999/client/login&response_type=code&state=JUkFF6
Cookie: JSESSIONID=0517E6F4E263721C8E75A1378D51179F
Form item: "user_oauth_approval" = "true"
Form item: "authorize" = "Authorize"
Server creates a new session, generates a random code
, and redirects the
response to redirect_uri
supplied on step 7.
HTTP/1.1 302
Set-Cookie: JSESSIONID=6503F28F4068C83EC7BE0F5EC163D06F;path=/;HttpOnly
Cache-Control: no-store
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
X-Frame-Options: DENY
Strict-Transport-Security: max-age=31536000 ; includeSubDomains
Location: http://localhost:9999/client/login?code=WEJz0f&state=JUkFF6
Content-Language: en-US
Content-Length: 0
Browser, with Client and Server sessions saved in cookies, gets the redirected Client URI.
GET /client/login?code=WEJz0f&state=JUkFF6 HTTP/1.1
Host: localhost:9999
Referer: http://localhost:8080/oauth/authorize?client_id=my-trusted-client&redirect_uri=http://localhost:9999/client/login&response_type=code&state=JUkFF6
Cookie: JSESSIONID=67CB7BD60760EDD84BD0884FF9F09651; JSESSIONID=6503F28F4068C83EC7BE0F5EC163D06F
Spring OAuth Client requests access token implementing
RFC6749 Section 4.1.3. Client authenticates itself through
Authorization
header passing credentials configured in Client application
properties.
POST /oauth/token HTTP/1.1
Authorization: Basic bXktdHJ1c3RlZC1jbGllbnQ6bXktdHJ1c3RlZC1jbGllbnQtcGFzcw==
Accept: application/json, application/x-www-form-urlencoded
Content-Type: application/x-www-form-urlencoded
Cache-Control: no-cache
Pragma: no-cache
User-Agent: Java/1.8.0_112
Host: localhost:8080
Connection: keep-alive
Content-Length: 101
Form item: "grant_type" = "authorization_code"
Form item: "code" = "WEJz0f"
Form item: "redirect_uri" = "http://localhost:9999/client/login"
Server verifies Client credentials stored in the database and
exchanges authorization code for access token implementing
RFC6749 Section 4.1.4. scope
is populated from the
corresponding database column.
HTTP/1.1 200
Cache-Control: no-store
Pragma: no-cache
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
X-Frame-Options: DENY
Content-Type: application/json;charset=UTF-8
{
access_token: 16d91f9e-69c5-41a9-bd0f-df8308589784,
token_type: bearer,
refresh_token: 8a283a19-7524-43d7-95bd-bc3a76260f3d,
expires_in: 59,
scope: read write trust
}
Although it’s not a part of RFC6749, Client sends access token verification
request to Server. Once again Client authenticates itself through
Authorization
header.
POST /oauth/check_token HTTP/1.1
Accept: application/json, application/*+json
Authorization: Basic bXktdHJ1c3RlZC1jbGllbnQ6bXktdHJ1c3RlZC1jbGllbnQtcGFzcw==
Content-Type: application/x-www-form-urlencoded
User-Agent: Java/1.8.0_112
Host: localhost:8080
Form item: "token" = "16d91f9e-69c5-41a9-bd0f-df8308589784"
Server verifies Client credentials and access token and responds with User/Client metadata.
HTTP/1.1 200
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Type: application/json;charset=UTF-8
{
exp: 1481467593
user_name: dave
authorities: [ROLE_USER]
client_id: my-trusted-client
scope: [read, write, trust]
}
Client redirects User browser to the URL requested initially on step 1. It also resets the session.
HTTP/1.1 302
Set-Cookie: JSESSIONID=460BEFFA884E6349212F8D2EA50BF776;path=/client;HttpOnly
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Location: http://localhost:9999/client/resource
This step is the same as step 1 but with valid Client and Server sessions.
GET /client/resource HTTP/1.1
Host: localhost:9999
Referer: http://localhost:8080/oauth/authorize?client_id=my-trusted-client&redirect_uri=http://localhost:9999/client/login&response_type=code&state=JUkFF6
Cookie: JSESSIONID=460BEFFA884E6349212F8D2EA50BF776; JSESSIONID=6503F28F4068C83EC7BE0F5EC163D06F
As a part of business logic Client sends a request to Resource server suppling bearer token as required by RFC6750 Section 2.
GET /me HTTP/1.1
Authorization: bearer 16d91f9e-69c5-41a9-bd0f-df8308589784
Accept: text/plain, application/json, application/*+json, */*
User-Agent: Java/1.8.0_112
Host: localhost:8888
Resource server calls OAuth Server to verify the access token implementing
RFC7662 Section 2.1. Resource authenticates itself through
Authorization
header suppling credentials configured in its application
properties.
POST /oauth/check_token HTTP/1.1
Accept: application/json, application/*+json
Authorization: Basic bXktcmVzb3VyY2U6bXktcmVzb3VyY2UtcGFzcw==
Content-Type: application/x-www-form-urlencoded
User-Agent: Java/1.8.0_112
Host: localhost:8080
Form item: "token" = "16d91f9e-69c5-41a9-bd0f-df8308589784"
Server verifies Resource credentials and access token and responds with User/Client metadata.
HTTP/1.1 200
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Type: application/json;charset=UTF-8
{
exp: 1481467593
user_name: dave
authorities: [ROLE_USER]
client_id: my-trusted-client
scope: [read, write, trust]
}
Since the access token is valid Resource server replies to Client
HTTP/1.1 200
Set-Cookie: JSESSIONID=4D7F3BE1EFD37A99D9878A0037A0F8A5;path=/;HttpOnly
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Type: application/json;charset=UTF-8
{name:dave}
Client returns the response to User
HTTP/1.1 200
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Type: text/html;charset=UTF-8
Specifications
- RFC6749 The OAuth 2.0 Authorization Framework
- RFC6750 The OAuth 2.0 Authorization Framework: Bearer Token Usage
- RFC6819 OAuth 2.0 Threat Model and Security Considerations
- RFC7009 OAuth 2.0 Token Revocation
- RFC7591 OAuth 2.0 Dynamic Client Registration Protocol
- RFC7636 PKCE by OAuth Public Clients
- RFC7662 OAuth 2.0 Token Introspection