lib/rodauth/features/oidc.rb in rodauth-oauth-0.2.0 vs lib/rodauth/features/oidc.rb in rodauth-oauth-0.3.0
- old
+ new
@@ -8,10 +8,58 @@
"email" => %i[email email_verified].freeze,
"address" => %i[address].freeze,
"phone" => %i[phone_number phone_number_verified].freeze
}.freeze
+ VALID_METADATA_KEYS = %i[
+ issuer
+ authorization_endpoint
+ token_endpoint
+ userinfo_endpoint
+ jwks_uri
+ registration_endpoint
+ scopes_supported
+ response_types_supported
+ response_modes_supported
+ grant_types_supported
+ acr_values_supported
+ subject_types_supported
+ id_token_signing_alg_values_supported
+ id_token_encryption_alg_values_supported
+ id_token_encryption_enc_values_supported
+ userinfo_signing_alg_values_supported
+ userinfo_encryption_alg_values_supported
+ userinfo_encryption_enc_values_supported
+ request_object_signing_alg_values_supported
+ request_object_encryption_alg_values_supported
+ request_object_encryption_enc_values_supported
+ token_endpoint_auth_methods_supported
+ token_endpoint_auth_signing_alg_values_supported
+ display_values_supported
+ claim_types_supported
+ claims_supported
+ service_documentation
+ claims_locales_supported
+ ui_locales_supported
+ claims_parameter_supported
+ request_parameter_supported
+ request_uri_parameter_supported
+ require_request_uri_registration
+ op_policy_uri
+ op_tos_uri
+ ].freeze
+
+ REQUIRED_METADATA_KEYS = %i[
+ issuer
+ authorization_endpoint
+ token_endpoint
+ jwks_uri
+ response_types_supported
+ subject_types_supported
+ id_token_signing_alg_values_supported
+ ].freeze
+
depends :oauth_jwt
auth_value_method :oauth_application_default_scope, "openid"
auth_value_method :oauth_application_scopes, %w[openid]
@@ -20,16 +68,51 @@
auth_value_method :invalid_scope_message, "The Access Token expired"
auth_value_method :webfinger_relation, "http://openid.net/specs/connect/1.0/issuer"
+ auth_value_method :oauth_prompt_login_cookie_key, "_rodauth_oauth_prompt_login"
+ auth_value_method :oauth_prompt_login_cookie_options, {}.freeze
+ auth_value_method :oauth_prompt_login_interval, 5 * 60 * 60 # 5 minutes
+
auth_value_methods(:get_oidc_param)
+ # /userinfo
+ route(:userinfo) do |r|
+ next unless is_authorization_server?
+
+ r.on method: %i[get post] do
+ catch_error do
+ oauth_token = authorization_token
+
+ throw_json_response_error(authorization_required_error_status, "invalid_token") unless oauth_token
+
+ oauth_scopes = oauth_token["scope"].split(" ")
+
+ throw_json_response_error(authorization_required_error_status, "invalid_token") unless oauth_scopes.include?("openid")
+
+ account = db[accounts_table].where(account_id_column => oauth_token["sub"]).first
+
+ throw_json_response_error(authorization_required_error_status, "invalid_token") unless account
+
+ oauth_scopes.delete("openid")
+
+ oidc_claims = { "sub" => oauth_token["sub"] }
+
+ fill_with_account_claims(oidc_claims, account, oauth_scopes)
+
+ json_response_success(oidc_claims)
+ end
+
+ throw_json_response_error(authorization_required_error_status, "invalid_token")
+ end
+ end
+
def openid_configuration(issuer = nil)
request.on(".well-known/openid-configuration") do
request.get do
- json_response_success(openid_configuration_body(issuer))
+ json_response_success(openid_configuration_body(issuer), cache: true)
end
end
end
def webfinger
@@ -55,10 +138,72 @@
end
end
private
+ def require_authorizable_account
+ try_prompt if param_or_nil("prompt")
+ super
+ end
+
+ # this executes before checking for a logged in account
+ def try_prompt
+ prompt = param_or_nil("prompt")
+
+ case prompt
+ when "none"
+ redirect_response_error("login_required") unless logged_in?
+
+ require_account
+
+ if db[oauth_grants_table].where(
+ oauth_grants_account_id_column => account_id,
+ oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column],
+ oauth_grants_redirect_uri_column => redirect_uri,
+ oauth_grants_scopes_column => scopes.join(oauth_scope_separator),
+ oauth_grants_access_type_column => "online"
+ ).count.zero?
+ redirect_response_error("consent_required")
+ end
+
+ request.env["REQUEST_METHOD"] = "POST"
+ when "login"
+ if logged_in? && request.cookies[oauth_prompt_login_cookie_key] == "login"
+ ::Rack::Utils.delete_cookie_header!(response.headers, oauth_prompt_login_cookie_key, oauth_prompt_login_cookie_options)
+ return
+ end
+
+ # logging out
+ clear_session
+ set_session_value(login_redirect_session_key, request.fullpath)
+
+ login_cookie_opts = Hash[oauth_prompt_login_cookie_options]
+ login_cookie_opts[:value] = "login"
+ login_cookie_opts[:expires] = convert_timestamp(Time.now + oauth_prompt_login_interval) # 15 minutes
+ ::Rack::Utils.set_cookie_header!(response.headers, oauth_prompt_login_cookie_key, login_cookie_opts)
+
+ redirect require_login_redirect
+ when "consent"
+ require_account
+
+ if db[oauth_grants_table].where(
+ oauth_grants_account_id_column => account_id,
+ oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column],
+ oauth_grants_redirect_uri_column => redirect_uri,
+ oauth_grants_scopes_column => scopes.join(oauth_scope_separator),
+ oauth_grants_access_type_column => "online"
+ ).count.zero?
+ redirect_response_error("consent_required")
+ end
+ when "select-account"
+ # obly works if select_account plugin is available
+ require_select_account if respond_to?(:require_select_account)
+ else
+ redirect_response_error("invalid_request")
+ end
+ end
+
def create_oauth_grant(create_params = {})
return super unless (nonce = param_or_nil("nonce"))
super(oauth_grants_nonce_column => nonce)
end
@@ -188,11 +333,13 @@
end
# Metadata
def openid_configuration_body(path)
- metadata = oauth_server_metadata_body(path)
+ metadata = oauth_server_metadata_body(path).select do |k, _|
+ VALID_METADATA_KEYS.include?(k)
+ end
scope_claims = oauth_application_scopes.each_with_object([]) do |scope, claims|
oidc, param = scope.split(".", 2)
if param
claims << param
@@ -202,66 +349,40 @@
end
end
scope_claims.unshift("auth_time") if last_account_login_at
- metadata.merge({
- userinfo_endpoint: userinfo_url,
- response_types_supported: metadata[:response_types_supported] +
- ["none", "id_token", %w[code token], %w[code id_token], %w[id_token token], %w[code id_token token]],
- response_modes_supported: %w[query fragment],
- grant_types_supported: %w[authorization_code implicit],
+ metadata.merge(
+ userinfo_endpoint: userinfo_url,
+ response_types_supported: metadata[:response_types_supported] +
+ ["none", "id_token", "code token", "code id_token", "id_token token", "code id_token token"],
+ response_modes_supported: %w[query fragment],
+ grant_types_supported: %w[authorization_code implicit],
- subject_types_supported: [oauth_jwt_subject_type],
+ subject_types_supported: [oauth_jwt_subject_type],
- id_token_signing_alg_values_supported: metadata[:token_endpoint_auth_signing_alg_values_supported],
- id_token_encryption_alg_values_supported: [oauth_jwt_jwe_algorithm].compact,
- id_token_encryption_enc_values_supported: [oauth_jwt_jwe_encryption_method].compact,
+ id_token_signing_alg_values_supported: metadata[:token_endpoint_auth_signing_alg_values_supported],
+ id_token_encryption_alg_values_supported: [oauth_jwt_jwe_algorithm].compact,
+ id_token_encryption_enc_values_supported: [oauth_jwt_jwe_encryption_method].compact,
- userinfo_signing_alg_values_supported: [],
- userinfo_encryption_alg_values_supported: [],
- userinfo_encryption_enc_values_supported: [],
+ userinfo_signing_alg_values_supported: [],
+ userinfo_encryption_alg_values_supported: [],
+ userinfo_encryption_enc_values_supported: [],
- request_object_signing_alg_values_supported: [],
- request_object_encryption_alg_values_supported: [],
- request_object_encryption_enc_values_supported: [],
+ request_object_signing_alg_values_supported: [],
+ request_object_encryption_alg_values_supported: [],
+ request_object_encryption_enc_values_supported: [],
- # These Claim Types are described in Section 5.6 of OpenID Connect Core 1.0 [OpenID.Core].
- # Values defined by this specification are normal, aggregated, and distributed.
- # If omitted, the implementation supports only normal Claims.
- claim_types_supported: %w[normal],
- claims_supported: %w[sub iss iat exp aud] | scope_claims
- })
- end
-
- # /userinfo
- route(:userinfo) do |r|
- next unless is_authorization_server?
-
- r.on method: %i[get post] do
- catch_error do
- oauth_token = authorization_token
-
- throw_json_response_error(authorization_required_error_status, "invalid_token") unless oauth_token
-
- oauth_scopes = oauth_token["scope"].split(" ")
-
- throw_json_response_error(authorization_required_error_status, "invalid_token") unless oauth_scopes.include?("openid")
-
- account = db[accounts_table].where(account_id_column => oauth_token["sub"]).first
-
- throw_json_response_error(authorization_required_error_status, "invalid_token") unless account
-
- oauth_scopes.delete("openid")
-
- oidc_claims = { "sub" => oauth_token["sub"] }
-
- fill_with_account_claims(oidc_claims, account, oauth_scopes)
-
- json_response_success(oidc_claims)
- end
-
- throw_json_response_error(authorization_required_error_status, "invalid_token")
+ # These Claim Types are described in Section 5.6 of OpenID Connect Core 1.0 [OpenID.Core].
+ # Values defined by this specification are normal, aggregated, and distributed.
+ # If omitted, the implementation supports only normal Claims.
+ claim_types_supported: %w[normal],
+ claims_supported: %w[sub iss iat exp aud] | scope_claims
+ ).reject do |key, val|
+ # Filter null values in optional items
+ (!REQUIRED_METADATA_KEYS.include?(key.to_sym) && val.nil?) ||
+ # Claims with zero elements MUST be omitted from the response
+ (val.respond_to?(:empty?) && val.empty?)
end
end
end
end