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