-
Notifications
You must be signed in to change notification settings - Fork 1k
Fixes #25809 - JWT auth for external users #6549
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
Changes from all commits
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 |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| class OidcJwt < JwtToken | ||
| def decode | ||
| return if token.blank? | ||
| OidcJwtValidate.new(token).decoded_payload | ||
| end | ||
| end |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| class OidcJwtValidate | ||
| attr_reader :decoded_token | ||
| delegate :logger, to: :Rails | ||
|
|
||
| def initialize(jwt_token) | ||
| @jwt_token = jwt_token | ||
| end | ||
|
|
||
| def decoded_payload | ||
| # OpenSSL#set_key method does not support ruby version < 2.4.0, apparently the JWT gem uses | ||
| # OpenSSL#set_key method for all ruby version. We must remove this condition once new version | ||
| # of the JWT(2.2.2) is released. | ||
| unless OpenSSL::PKey::RSA.new.respond_to?(:set_key) | ||
| Foreman::Logging.logger('app').error "SSO feature is not available for Ruby < 2.4.0" | ||
| return nil | ||
| end | ||
| JWT.decode(@jwt_token, nil, true, | ||
| { aud: Setting['oidc_audience'], | ||
| verify_aud: true, | ||
| iss: Setting['oidc_issuer'], | ||
| verify_iss: true, | ||
| algorithms: [Setting['oidc_algorithm']], | ||
|
rabajaj0509 marked this conversation as resolved.
|
||
| jwks: jwks_loader } | ||
| ).first | ||
| rescue JWT::DecodeError => e | ||
| Foreman::Logging.exception('Failed to decode JWT', e) | ||
| nil | ||
| end | ||
|
|
||
| private | ||
|
|
||
| def jwks_loader(options = {}) | ||
| response = RestClient::Request.execute( | ||
| :url => Setting['oidc_jwks_url'], | ||
| :method => :get, | ||
| :verify_ssl => true | ||
| ) | ||
| json_response = JSON.parse(response) | ||
| if json_response.is_a?(Hash) | ||
| jwks_keys = json_response['keys'] | ||
| { keys: jwks_keys.map(&:symbolize_keys) } | ||
| else | ||
| Foreman::Logging.logger('app').error "Invalid JWKS response." | ||
| {} | ||
| end | ||
| rescue RestClient::Exception, SocketError, JSON::ParserError => e | ||
| Foreman::Logging.exception('Failed to load the JWKS', e) | ||
| {} | ||
| end | ||
| end | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| module SSO | ||
| class OpenidConnect < Base | ||
| delegate :session, :to => :controller | ||
| attr_reader :current_user | ||
|
|
||
| def available? | ||
| controller.api_request? && bearer_token_set? && valid_issuer? | ||
| end | ||
|
|
||
| def authenticate! | ||
| payload = jwt_token.decode | ||
| return nil if payload.nil? | ||
| user = find_or_create_user_from_jwt(payload) | ||
| @current_user = user | ||
| update_session(payload) | ||
| user&.login | ||
| end | ||
|
|
||
| def authenticated? | ||
| self.user = User.current.presence || authenticate! | ||
| end | ||
|
|
||
| private | ||
|
|
||
| def jwt_token | ||
|
Member
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. I'd still like to avoid this naming (https://en.wikipedia.org/wiki/RAS_syndrome) but I won't block on it.
Member
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. I understand but we're already using that in multiple places and it seems pretty common in general (e.g. https://www.google.com/search?q=%22jwt+token%22). |
||
| @jwt_token ||= jwt_token_from_request | ||
| end | ||
|
|
||
| def jwt_token_from_request | ||
| token = request.authorization.split(' ')[1] | ||
| OidcJwt.new(token) | ||
| end | ||
|
|
||
| def bearer_token_set? | ||
| request.authorization.present? && request.authorization.start_with?('Bearer') | ||
| end | ||
|
|
||
| def valid_issuer? | ||
| payload = jwt_token.decoded_payload | ||
| payload.key?('iss') && (payload['iss'] == Setting['oidc_issuer']) | ||
| end | ||
|
|
||
| def update_session(payload) | ||
| session[:sso_method] = self.class.to_s | ||
| session[:expires_at] = payload['exp'] | ||
| end | ||
|
|
||
| def find_or_create_user_from_jwt(payload) | ||
| User.find_or_create_external_user( | ||
| { login: payload['preferred_username'], | ||
|
timogoebel marked this conversation as resolved.
|
||
| mail: payload['email'], | ||
| firstname: payload['given_name'], | ||
| lastname: payload['family_name']}, | ||
| Setting['authorize_login_delegation_auth_source_user_autocreate'] | ||
| ) | ||
| end | ||
| end | ||
| end | ||
Uh oh!
There was an error while loading. Please reload this page.