-
Notifications
You must be signed in to change notification settings - Fork 124
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feature: OIDC refresh token support #2664
Changes from 3 commits
d5fff30
265cf02
127967c
79eca30
cce24bf
c4400b9
b8dde67
5479948
f7ac424
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,13 +9,14 @@ def oidc_authenticate_code_redirect | |
# params. This will likely need to be done via the Handler. | ||
params.permit! | ||
|
||
auth_token = Authentication::Handler::AuthenticationHandler.new( | ||
auth_token, headers_h = Authentication::Handler::AuthenticationHandler.new( | ||
authenticator_type: params[:authenticator] | ||
).call( | ||
parameters: params.to_hash.symbolize_keys, | ||
request_ip: request.ip | ||
) | ||
|
||
set_headers(headers_h) | ||
render_authn_token(auth_token) | ||
rescue => e | ||
log_backtrace(e) | ||
|
@@ -248,6 +249,12 @@ def render_authn_token(authn_token) | |
render(content_type => authn_token) | ||
end | ||
|
||
def set_headers(headers_h) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do not prefix writer method names with |
||
headers_h.each { |key, value| | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Avoid using |
||
response.set_header(key, value.to_s) unless key.blank? || value.blank? | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. AuthenticateController#set_headers refers to 'value' more than self (maybe move it to another class?) |
||
} | ||
end | ||
|
||
def log_audit_success( | ||
authn_params:, | ||
audit_event_class: | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -37,11 +37,28 @@ def oidc_client | |
end | ||
end | ||
|
||
def callback(code:, nonce:, code_verifier:) | ||
def get_token_with_code(code:, nonce:, code_verifier:) | ||
oidc_client.authorization_code = code | ||
id_token, refresh_token = get_token_pair(code_verifier) | ||
decoded_id_token = decode_id_token(id_token) | ||
verify_id_token(decoded_id_token, nonce) | ||
|
||
[decoded_id_token, refresh_token] | ||
end | ||
|
||
def get_token_with_refresh_token(refresh_token:, nonce:) | ||
oidc_client.refresh_token = refresh_token | ||
id_token, refresh_token = get_token_pair(nil) | ||
decoded_id_token = decode_id_token(id_token) | ||
verify_id_token(decoded_id_token, nonce, refresh: true) | ||
|
||
[decoded_id_token, refresh_token] | ||
end | ||
|
||
def get_token_pair(code_verifier) | ||
begin | ||
bearer_token = oidc_client.access_token!( | ||
scope: true, | ||
scope: @authenticator.scope, | ||
client_auth_method: :basic, | ||
code_verifier: code_verifier | ||
) | ||
|
@@ -54,11 +71,19 @@ def callback(code:, nonce:, code_verifier:) | |
when /The authorization code is invalid or has expired/ | ||
raise Errors::Authentication::AuthnOidc::TokenRetrievalFailed, | ||
'Authorization code is invalid or has expired' | ||
when /The refresh token is invalid or expired/ | ||
raise Errors::Authentication::AuthnOidc::TokenRetrievalFailed, | ||
'Refresh token is invalid or has expired' | ||
end | ||
raise e | ||
end | ||
|
||
id_token = bearer_token.id_token || bearer_token.access_token | ||
refresh_token = bearer_token.refresh_token | ||
[id_token, refresh_token] | ||
end | ||
|
||
def decode_id_token(id_token) | ||
begin | ||
attempts ||= 0 | ||
decoded_id_token = @oidc_id_token.decode( | ||
|
@@ -76,11 +101,20 @@ def callback(code:, nonce:, code_verifier:) | |
retry | ||
end | ||
|
||
decoded_id_token | ||
end | ||
|
||
def verify_id_token(decoded_id_token, nonce, refresh: false) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Authentication::AuthnOidc::V2::Client#verify_id_token has boolean parameter 'refresh' |
||
expected_nonce = nonce | ||
if refresh && decoded_id_token.raw_attributes['nonce'].nil? | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Authentication::AuthnOidc::V2::Client#verify_id_token is controlled by argument 'refresh' There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Authentication::AuthnOidc::V2::Client#verify_id_token performs a nil-check |
||
expected_nonce = nil | ||
end | ||
|
||
begin | ||
decoded_id_token.verify!( | ||
issuer: @authenticator.provider_uri, | ||
client_id: @authenticator.client_id, | ||
nonce: nonce | ||
nonce: expected_nonce | ||
) | ||
rescue OpenIDConnect::ResponseObject::IdToken::InvalidNonce | ||
raise Errors::Authentication::AuthnOidc::TokenVerificationFailed, | ||
|
@@ -92,7 +126,6 @@ def callback(code:, nonce:, code_verifier:) | |
raise Errors::Authentication::AuthnOidc::TokenVerificationFailed, | ||
e.message | ||
end | ||
decoded_id_token | ||
end | ||
|
||
def discovery_information(invalidate: false) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -21,18 +21,19 @@ def callback(args) | |
end | ||
end | ||
|
||
identity = resolve_identity( | ||
jwt: @client.callback( | ||
code: args[:code], | ||
nonce: args[:nonce], | ||
code_verifier: args[:code_verifier] | ||
) | ||
jwt, refresh_token = @client.get_token_with_code( | ||
code: args[:code], | ||
nonce: args[:nonce], | ||
code_verifier: args[:code_verifier] | ||
) | ||
identity = resolve_identity(jwt: jwt) | ||
unless identity.present? | ||
raise Errors::Authentication::AuthnOidc::IdTokenClaimNotFoundOrEmpty, | ||
@authenticator.claim_mapping | ||
end | ||
identity | ||
|
||
return [identity, {}] if refresh_token.nil? | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Authentication::AuthnOidc::V2::Strategy#callback performs a nil-check There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add empty line after guard clause. |
||
[identity, { 'X-OIDC-Refresh-Token' => refresh_token }] | ||
end | ||
|
||
def resolve_identity(jwt:) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -62,6 +62,14 @@ Feature: OIDC Authenticator V2 - Users can authenticate with OIDC authenticator | |
cucumber:user:alice successfully authenticated with authenticator authn-oidc service cucumber:webservice:conjur/authn-oidc/keycloak2 | ||
""" | ||
|
||
@smoke | ||
Scenario: A valid code to get Conjur access token and OIDC refresh token | ||
Given I enable OIDC V2 refresh token flows for "keycloak2" | ||
And I fetch a code for username "alice" and password "alice" | ||
When I authenticate via OIDC V2 with code | ||
Then user "alice" has been authorized by Conjur | ||
And The authentication response includes header "X-OIDC-Refresh-Token" | ||
|
||
@smoke | ||
Scenario: A valid code with email as claim mapping | ||
Given I extend the policy with: | ||
|
@@ -189,7 +197,7 @@ Feature: OIDC Authenticator V2 - Users can authenticate with OIDC authenticator | |
Given I save my place in the log file | ||
And I fetch a code for username "[email protected]" and password "alice" | ||
When I authenticate via OIDC V2 with code "bad-code" | ||
Then it is a bad request | ||
Then it is unauthorized | ||
And The following appears in the log after my savepoint: | ||
""" | ||
Rack::OAuth2::Client::Error | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reconsidering if this is the interface we want. Authn-OIDC V2 is our only auth scheme that needs to set a response header - this means each other auth scheme converted to use the
AuthenticationHandler
pattern will be returning an empty set of headers.To avoid passing the
response
object everywhere, I experimented with passing a lambda as an optional parameter to thecall
method, so that while other auth schemes can do this:... Authn-OIDC V2 could do this:
... and the
set_header
block could be used in the OIDC-V2-specificAuthentication::AuthnOidc::V2::Strategy
class.