PKCE is now default enabled. You can now create public / no-secret apps OIDC apps

This commit is contained in:
Dan Milne
2025-12-31 09:22:18 +11:00
parent 00eca6d8b2
commit cc7beba9de
15 changed files with 456 additions and 64 deletions

View File

@@ -26,16 +26,17 @@ module Admin
@application.allowed_groups = Group.where(id: group_ids)
end
# Get the plain text client secret to show one time
# Get the plain text client secret to show one time (confidential clients only)
client_secret = nil
if @application.oidc?
if @application.oidc? && @application.confidential_client?
client_secret = @application.generate_new_client_secret!
end
if @application.oidc? && client_secret
if @application.oidc?
flash[:notice] = "Application created successfully."
flash[:client_id] = @application.client_id
flash[:client_secret] = client_secret
flash[:client_secret] = client_secret if client_secret
flash[:public_client] = true if @application.public_client?
else
flash[:notice] = "Application created successfully."
end
@@ -74,15 +75,20 @@ module Admin
def regenerate_credentials
if @application.oidc?
# Generate new client ID and secret
# Generate new client ID (always)
new_client_id = SecureRandom.urlsafe_base64(32)
client_secret = @application.generate_new_client_secret!
@application.update!(client_id: new_client_id)
flash[:notice] = "Credentials regenerated successfully."
flash[:client_id] = @application.client_id
flash[:client_secret] = client_secret
# Generate new client secret only for confidential clients
if @application.confidential_client?
client_secret = @application.generate_new_client_secret!
flash[:client_secret] = client_secret
else
flash[:public_client] = true
end
redirect_to admin_application_path(@application)
else
@@ -97,15 +103,24 @@ module Admin
end
def application_params
params.require(:application).permit(
permitted = params.require(:application).permit(
:name, :slug, :app_type, :active, :redirect_uris, :description, :metadata,
:domain_pattern, :landing_url, :access_token_ttl, :refresh_token_ttl, :id_token_ttl,
:icon, :backchannel_logout_uri,
headers_config: {}
).tap do |whitelisted|
# Remove client_secret from params if present (shouldn't be updated via form)
whitelisted.delete(:client_secret)
:icon, :backchannel_logout_uri, :is_public_client, :require_pkce
)
# Handle headers_config - it comes as a JSON string from the text area
if params[:application][:headers_config].present?
begin
permitted[:headers_config] = JSON.parse(params[:application][:headers_config])
rescue JSON::ParserError
permitted[:headers_config] = {}
end
end
# Remove client_secret from params if present (shouldn't be updated via form)
permitted.delete(:client_secret)
permitted
end
end
end

View File

@@ -296,22 +296,33 @@ class OidcController < ApplicationController
end
def handle_authorization_code_grant
# Get client credentials from Authorization header or params
client_id, client_secret = extract_client_credentials
unless client_id && client_secret
render json: { error: "invalid_client" }, status: :unauthorized
unless client_id
render json: { error: "invalid_client", error_description: "client_id is required" }, status: :unauthorized
return
end
# Find and validate the application
# Find the application
application = Application.find_by(client_id: client_id)
unless application && application.authenticate_client_secret(client_secret)
render json: { error: "invalid_client" }, status: :unauthorized
unless application
render json: { error: "invalid_client", error_description: "Unknown client" }, status: :unauthorized
return
end
# Validate client credentials based on client type
if application.public_client?
# Public clients don't have a secret - they MUST use PKCE (checked later)
Rails.logger.info "OAuth: Public client authentication for #{application.name}"
else
# Confidential clients MUST provide valid client_secret
unless client_secret.present? && application.authenticate_client_secret(client_secret)
render json: { error: "invalid_client", error_description: "Invalid client credentials" }, status: :unauthorized
return
end
end
# Check if application is active
unless application.active?
Rails.logger.error "OAuth: Token request for inactive application: #{application.name}"
@@ -371,8 +382,8 @@ class OidcController < ApplicationController
return
end
# Validate PKCE if code challenge is present
pkce_result = validate_pkce(auth_code, code_verifier)
# Validate PKCE - required for public clients and optionally for confidential clients
pkce_result = validate_pkce(application, auth_code, code_verifier)
unless pkce_result[:valid]
render json: {
error: pkce_result[:error],
@@ -433,18 +444,30 @@ class OidcController < ApplicationController
# Get client credentials from Authorization header or params
client_id, client_secret = extract_client_credentials
unless client_id && client_secret
render json: { error: "invalid_client" }, status: :unauthorized
unless client_id
render json: { error: "invalid_client", error_description: "client_id is required" }, status: :unauthorized
return
end
# Find and validate the application
# Find the application
application = Application.find_by(client_id: client_id)
unless application && application.authenticate_client_secret(client_secret)
render json: { error: "invalid_client" }, status: :unauthorized
unless application
render json: { error: "invalid_client", error_description: "Unknown client" }, status: :unauthorized
return
end
# Validate client credentials based on client type
if application.public_client?
# Public clients don't have a secret
Rails.logger.info "OAuth: Public client refresh token request for #{application.name}"
else
# Confidential clients MUST provide valid client_secret
unless client_secret.present? && application.authenticate_client_secret(client_secret)
render json: { error: "invalid_client", error_description: "Invalid client credentials" }, status: :unauthorized
return
end
end
# Check if application is active
unless application.active?
Rails.logger.error "OAuth: Refresh token request for inactive application: #{application.name}"
@@ -716,11 +739,26 @@ class OidcController < ApplicationController
private
def validate_pkce(auth_code, code_verifier)
# Skip PKCE validation if no code challenge was stored (legacy clients)
return { valid: true } unless auth_code.code_challenge.present?
def validate_pkce(application, auth_code, code_verifier)
# Check if PKCE is required for this application
pkce_required = application.requires_pkce?
pkce_provided = auth_code.code_challenge.present?
# PKCE is required but no verifier provided
# If PKCE is required but wasn't provided during authorization
if pkce_required && !pkce_provided
client_type = application.public_client? ? "public clients" : "this application"
return {
valid: false,
error: "invalid_request",
error_description: "PKCE is required for #{client_type}. code_challenge must be provided during authorization.",
status: :bad_request
}
end
# Skip validation if no code challenge was stored (legacy clients without PKCE requirement)
return { valid: true } unless pkce_provided
# PKCE was provided during authorization but no verifier sent with token request
unless code_verifier.present?
return {
valid: false,