Skip to content

Commit

Permalink
Add PKCE support to redmine_oauth_controller
Browse files Browse the repository at this point in the history
  • Loading branch information
jukkakangas committed Jan 2, 2025
1 parent 41bb4e1 commit c66e647
Showing 1 changed file with 44 additions and 12 deletions.
56 changes: 44 additions & 12 deletions app/controllers/redmine_oauth_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@

require 'account_controller'
require 'jwt'
require 'securerandom'
require 'base64'
require 'digest'

# OAuth controller
class RedmineOauthController < AccountController
Expand All @@ -31,6 +34,12 @@ def oauth
session[:oauth_autologin] = params[:oauth_autologin]
oauth_csrf_token = generate_csrf_token
session[:oauth_csrf_token] = oauth_csrf_token

# Generate PKCE code_verifier and code_challenge
code_verifier = generate_code_verifier
session[:code_verifier] = code_verifier
code_challenge = generate_code_challenge(code_verifier)

case RedmineOauth.oauth_name
when 'Azure AD'
redirect_to oauth_client.auth_code.authorize_url(
Expand All @@ -41,37 +50,49 @@ def oauth
'openid profile email'
else
'user:email'
end
end,
code_challenge: code_challenge,
code_challenge_method: 'S256'
)
when 'GitLab'
redirect_to oauth_client.auth_code.authorize_url(
redirect_uri: oauth_callback_url,
state: oauth_csrf_token,
scope: 'read_user'
scope: 'read_user',
code_challenge: code_challenge,
code_challenge_method: 'S256'
)
when 'Google'
redirect_to oauth_client.auth_code.authorize_url(
redirect_uri: oauth_callback_url,
state: oauth_csrf_token,
scope: 'profile email'
scope: 'profile email',
code_challenge: code_challenge,
code_challenge_method: 'S256'
)
when 'Keycloak'
redirect_to oauth_client.auth_code.authorize_url(
redirect_uri: oauth_callback_url,
state: oauth_csrf_token,
scope: 'openid email'
scope: 'openid email',
code_challenge: code_challenge,
code_challenge_method: 'S256'
)
when 'Okta'
redirect_to oauth_client.auth_code.authorize_url(
redirect_uri: oauth_callback_url,
state: oauth_csrf_token,
scope: 'openid profile email'
scope: 'openid profile email',
code_challenge: code_challenge,
code_challenge_method: 'S256'
)
when 'Custom'
redirect_to oauth_client.auth_code.authorize_url(
redirect_uri: oauth_callback_url,
state: oauth_csrf_token,
scope: RedmineOauth.custom_scope
scope: RedmineOauth.custom_scope,
code_challenge: code_challenge,
code_challenge_method: 'S256'
)
else
flash['error'] = l(:oauth_invalid_provider)
Expand All @@ -89,31 +110,34 @@ def oauth_callback
raise StandardError, l(:notice_account_invalid_credentials)
end

# Retrieve the PKCE code_verifier from the session
code_verifier = session.delete(:code_verifier)

case RedmineOauth.oauth_name
when 'Azure AD'
token = oauth_client.auth_code.get_token(params['code'], redirect_uri: oauth_callback_url)
token = oauth_client.auth_code.get_token(params['code'], redirect_uri: oauth_callback_url, code_verifier: code_verifier)
user_info = JWT.decode(token.token, nil, false).first
email = user_info['unique_name']
when 'GitLab'
token = oauth_client.auth_code.get_token(params['code'], redirect_uri: oauth_callback_url)
token = oauth_client.auth_code.get_token(params['code'], redirect_uri: oauth_callback_url, code_verifier: code_verifier)
userinfo_response = token.get('/api/v4/user', headers: { 'Accept' => 'application/json' })
user_info = JSON.parse(userinfo_response.body)
user_info['login'] = user_info['username']
email = user_info['email']
when 'Google'
token = oauth_client.auth_code.get_token(params['code'], redirect_uri: oauth_callback_url)
token = oauth_client.auth_code.get_token(params['code'], redirect_uri: oauth_callback_url, code_verifier: code_verifier)
userinfo_response = token.get('https://openidconnect.googleapis.com/v1/userinfo',
headers: { 'Accept' => 'application/json' })
user_info = JSON.parse(userinfo_response.body)
user_info['login'] = user_info['email']
email = user_info['email']
when 'Keycloak'
token = oauth_client.auth_code.get_token(params['code'], redirect_uri: oauth_callback_url)
token = oauth_client.auth_code.get_token(params['code'], redirect_uri: oauth_callback_url, code_verifier: code_verifier)
user_info = JWT.decode(token.token, nil, false).first
user_info['login'] = user_info['preferred_username']
email = user_info['email']
when 'Okta'
token = oauth_client.auth_code.get_token(params['code'], redirect_uri: oauth_callback_url)
token = oauth_client.auth_code.get_token(params['code'], redirect_uri: oauth_callback_url, code_verifier: code_verifier)
userinfo_response = token.get(
"/oauth2/#{RedmineOauth.tenant_id}/v1/userinfo",
headers: { 'Accept' => 'application/json' }
Expand All @@ -122,7 +146,7 @@ def oauth_callback
user_info['login'] = user_info['preferred_username']
email = user_info['email']
when 'Custom'
token = oauth_client.auth_code.get_token(params['code'], redirect_uri: oauth_callback_url)
token = oauth_client.auth_code.get_token(params['code'], redirect_uri: oauth_callback_url, code_verifier: code_verifier)
if RedmineOauth.custom_profile_endpoint.empty?
user_info = JWT.decode(token.token, nil, false).first
else
Expand Down Expand Up @@ -186,6 +210,14 @@ def set_oauth_autologin_cookie

private

def generate_code_verifier
SecureRandom.urlsafe_base64(32)
end

def generate_code_challenge(code_verifier)
Base64.urlsafe_encode64(Digest::SHA256.digest(code_verifier)).delete("=")
end

def set_params
params['back_url'] = session[:back_url]
session.delete :back_url
Expand Down

0 comments on commit c66e647

Please sign in to comment.