Skip to content

Commit

Permalink
Add PKCE verification support
Browse files Browse the repository at this point in the history
  • Loading branch information
skycocker committed Nov 2, 2022
1 parent ec03912 commit cba682d
Show file tree
Hide file tree
Showing 3 changed files with 90 additions and 22 deletions.
41 changes: 22 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,24 +47,27 @@ config.omniauth :openid_connect, {

### Options Overview

| Field | Description | Required | Default | Example/Options |
|------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------|----------|----------------------------|-----------------------------------------------------|
| name | Arbitrary string to identify connection and identify it from other openid_connect providers | no | String: openid_connect | :my_idp |
| issuer | Root url for the authorization server | yes | | https://myprovider.com |
| discovery | Should OpenID discovery be used. This is recommended if the IDP provides a discovery endpoint. See client config for how to manually enter discovered values. | no | false | one of: true, false |
| client_auth_method | Which authentication method to use to authenticate your app with the authorization server | no | Sym: basic | "basic", "jwks" |
| scope | Which OpenID scopes to include (:openid is always required) | no | Array<sym> [:openid] | [:openid, :profile, :email] |
| response_type | Which OAuth2 response type to use with the authorization request | no | String: code | one of: 'code', 'id_token' |
| state | A value to be used for the OAuth2 state parameter on the authorization request. Can be a proc that generates a string. | no | Random 16 character string | Proc.new { SecureRandom.hex(32) } |
| response_mode | The response mode per [spec](https://openid.net/specs/oauth-v2-form-post-response-mode-1_0.html) | no | nil | one of: :query, :fragment, :form_post, :web_message |
| display | An optional parameter to the authorization request to determine how the authorization and consent page | no | nil | one of: :page, :popup, :touch, :wap |
| prompt | An optional parameter to the authrization request to determine what pages the user will be shown | no | nil | one of: :none, :login, :consent, :select_account |
| send_scope_to_token_endpoint | Should the scope parameter be sent to the authorization token endpoint? | no | true | one of: true, false |
| post_logout_redirect_uri | The logout redirect uri to use per the [session management draft](https://openid.net/specs/openid-connect-session-1_0.html) | no | empty | https://myapp.com/logout/callback |
| uid_field | The field of the user info response to be used as a unique id | no | 'sub' | "sub", "preferred_username" |
| extra_authorize_params | A hash of extra fixed parameters that will be merged to the authorization request | no | Hash | {"tenant" => "common"} |
| allow_authorize_params | A list of allowed dynamic parameters that will be merged to the authorization request | no | Array | [:screen_name] |
| client_options | A hash of client options detailed in its own section | yes | | |
| Field | Description | Required | Default | Example/Options |
|------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------|----------|-------------------------------|-----------------------------------------------------|
| name | Arbitrary string to identify connection and identify it from other openid_connect providers | no | String: openid_connect | :my_idp |
| issuer | Root url for the authorization server | yes | | https://myprovider.com |
| discovery | Should OpenID discovery be used. This is recommended if the IDP provides a discovery endpoint. See client config for how to manually enter discovered values. | no | false | one of: true, false |
| client_auth_method | Which authentication method to use to authenticate your app with the authorization server | no | Sym: basic | "basic", "jwks" |
| scope | Which OpenID scopes to include (:openid is always required) | no | Array<sym> [:openid] | [:openid, :profile, :email] |
| response_type | Which OAuth2 response type to use with the authorization request | no | String: code | one of: 'code', 'id_token' |
| state | A value to be used for the OAuth2 state parameter on the authorization request. Can be a proc that generates a string. | no | Random 16 character string | Proc.new { SecureRandom.hex(32) } |
| response_mode | The response mode per [spec](https://openid.net/specs/oauth-v2-form-post-response-mode-1_0.html) | no | nil | one of: :query, :fragment, :form_post, :web_message |
| display | An optional parameter to the authorization request to determine how the authorization and consent page | no | nil | one of: :page, :popup, :touch, :wap |
| prompt | An optional parameter to the authrization request to determine what pages the user will be shown | no | nil | one of: :none, :login, :consent, :select_account |
| send_scope_to_token_endpoint | Should the scope parameter be sent to the authorization token endpoint? | no | true | one of: true, false |
| post_logout_redirect_uri | The logout redirect uri to use per the [session management draft](https://openid.net/specs/openid-connect-session-1_0.html) | no | empty | https://myapp.com/logout/callback |
| uid_field | The field of the user info response to be used as a unique id | no | 'sub' | "sub", "preferred_username" |
| extra_authorize_params | A hash of extra fixed parameters that will be merged to the authorization request | no | Hash | {"tenant" => "common"} |
| allow_authorize_params | A list of allowed dynamic parameters that will be merged to the authorization request | no | Array | [:screen_name] |
| pkce | Enable [PKCE flow](https://oauth.net/2/pkce/) | no | false | one of: true, false |
| pkce_verifier | Specify a custom PKCE verifier code. | no | A random 128-char string | Proc.new { SecureRandom.hex(64) } |
| pkce_options | Specify a custom implementation of the PKCE code challenge/method. | no | SHA256(code_challenge) in hex | Proc to customise the code challenge generation |
| client_options | A hash of client options detailed in its own section | yes | | |

### Client Config Options

Expand Down Expand Up @@ -94,7 +97,7 @@ These are the configuration options for the client_options hash of the configura

* `response_type` tells the authorization server which grant type the application wants to use,
currently, only `:code` (Authorization Code grant) and `:id_token` (Implicit grant) are valid.
* If you want to pass `state` paramete by yourself. You can set Proc Object.
* If you want to pass `state` parameter by yourself. You can set Proc Object.
e.g. `state: Proc.new { SecureRandom.hex(32) }`
* `nonce` is optional. If don't want to pass "nonce" parameter to provider, You should specify
`false` to `send_nonce` option. (default true)
Expand Down
37 changes: 34 additions & 3 deletions lib/omniauth/strategies/openid_connect.rb
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@ class OpenIDConnect
option :extra_authorize_params, {}
option :allow_authorize_params, []
option :uid_field, 'sub'
option :pkce, false
option :pkce_verifier, nil
option :pkce_options, {
code_challenge: proc { |verifier| Digest::SHA2.hexdigest(verifier.call) },
code_challenge_method: 'S256',
}

def uid
user_info.raw_attributes[options.uid_field.to_sym] || user_info.sub
Expand Down Expand Up @@ -178,6 +184,11 @@ def authorize_uri
opts[key] = request.params[key.to_s] unless opts.key?(key)
end

if options.pkce
opts.merge!(pkce_authorize_params)
session['omniauth.pkce.verifier'] = options.pkce_verifier
end

client.authorization_uri(opts.reject { |_k, v| v.nil? })
end

Expand All @@ -187,6 +198,22 @@ def public_key
key_or_secret || config.jwks
end

def pkce_authorize_params
options.pkce_verifier = proc { SecureRandom.hex(64) } if options.pkce_verifier.nil?

# NOTE: see https://tools.ietf.org/html/rfc7636#appendix-A
{
code_challenge: options.pkce_options[:code_challenge].call(options.pkce_verifier),
code_challenge_method: options.pkce_options[:code_challenge_method],
}
end

def pkce_token_params
return {} unless options.pkce

{ code_verifier: session.delete('omniauth.pkce.verifier') }
end

private

def issuer
Expand Down Expand Up @@ -220,10 +247,14 @@ def user_info
def access_token
return @access_token if @access_token

@access_token = client.access_token!(
params = {
scope: (options.scope if options.send_scope_to_token_endpoint),
client_auth_method: options.client_auth_method
)
client_auth_method: options.client_auth_method,
}

params[:code_verifier] = session.delete('omniauth.pkce.verifier') if options.pkce

@access_token = client.access_token!(params)

verify_id_token!(@access_token.id_token) if configured_response_type == 'code'

Expand Down
34 changes: 34 additions & 0 deletions test/lib/omniauth/strategies/openid_connect_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -632,6 +632,40 @@ def test_id_token_auth_hash
assert auth_hash.key?('extra')
assert auth_hash['extra'].key?('raw_info')
end

def test_option_pkce
strategy.options.client_options[:host] = 'example.com'

# test pkce disabled
strategy.options.pkce = false

assert((strategy.authorize_uri !~ /code_challenge=/), 'URI must not contain code challenge param')
assert((strategy.authorize_uri !~ /code_challenge_method=/), 'URI must not contain code challenge method param')

# test pkce enabled with default opts
strategy.options.pkce = true

assert(strategy.authorize_uri =~ /code_challenge=/, 'URI must contain code challenge param')
assert(strategy.authorize_uri =~ /code_challenge_method=/, 'URI must contain code challenge method param')

# test pkce with custom verifier code
strategy.options.pkce_verifier = proc { 'dummy_verifier' }
code_challenge_value = Digest::SHA2.hexdigest(strategy.options.pkce_verifier.call)

assert(strategy.authorize_uri =~ /#{Regexp.quote(code_challenge_value)}/, 'URI must contain code challenge value')

# test pkce with custom options and plain text code
strategy.options.pkce_options =
{
code_challenge: proc { |verifier|
verifier.call
},
code_challenge_method: 'plain',
}

assert(strategy.authorize_uri =~ /#{Regexp.quote(strategy.options.pkce_verifier.call)}/,
'URI must contain code challenge value')
end
end
end
end

0 comments on commit cba682d

Please sign in to comment.