lib/rodauth/features/oidc.rb in rodauth-oauth-0.9.3 vs lib/rodauth/features/oidc.rb in rodauth-oauth-0.10.0
- old
+ new
@@ -58,11 +58,11 @@
response_types_supported
subject_types_supported
id_token_signing_alg_values_supported
].freeze
- depends :oauth_jwt
+ depends :account_expiration, :oauth_jwt
auth_value_method :oauth_application_default_scope, "openid"
auth_value_method :oauth_application_scopes, %w[openid]
auth_value_method :oauth_applications_id_token_signed_response_alg_column, :id_token_signed_response_alg
@@ -71,12 +71,13 @@
auth_value_method :oauth_applications_userinfo_signed_response_alg_column, :userinfo_signed_response_alg
auth_value_method :oauth_applications_userinfo_encrypted_response_alg_column, :userinfo_encrypted_response_alg
auth_value_method :oauth_applications_userinfo_encrypted_response_enc_column, :userinfo_encrypted_response_enc
auth_value_method :oauth_grants_nonce_column, :nonce
+ auth_value_method :oauth_grants_acr_column, :acr
auth_value_method :oauth_tokens_nonce_column, :nonce
- auth_value_method :oauth_tokens_auth_time_column, :auth_time
+ auth_value_method :oauth_tokens_acr_column, :acr
translatable_method :invalid_scope_message, "The Access Token expired"
auth_value_method :webfinger_relation, "http://openid.net/specs/connect/1.0/issuer"
@@ -86,11 +87,17 @@
# logout
auth_value_method :oauth_applications_post_logout_redirect_uri_column, :post_logout_redirect_uri
auth_value_method :use_rp_initiated_logout?, false
- auth_value_methods(:get_oidc_param, :get_additional_param)
+ auth_value_methods(
+ :get_oidc_param,
+ :get_additional_param,
+ :require_acr_value_phr,
+ :require_acr_value_phrh,
+ :require_acr_value
+ )
# /userinfo
route(:userinfo) do |r|
next unless is_authorization_server?
@@ -250,18 +257,47 @@
end
end
private
+ if defined?(::I18n)
+ def before_authorize_route
+ if (ui_locales = param_or_nil("ui_locales"))
+ ui_locales = ui_locales.split(" ").map(&:to_sym)
+ ui_locales &= ::I18n.available_locales
+
+ ::I18n.locale = ui_locales.first unless ui_locales.empty?
+ end
+
+ super
+ end
+ end
+
+ def validate_oauth_grant_params
+ return super unless (max_age = param_or_nil("max_age"))
+
+ max_age = Integer(max_age)
+
+ redirect_response_error("invalid_request") unless max_age.positive?
+
+ return unless Time.now - last_account_login_at > max_age
+
+ # force user to re-login
+ clear_session
+ set_session_value(login_redirect_session_key, request.fullpath)
+ redirect require_login_redirect
+ end
+
def require_authorizable_account
- try_prompt if param_or_nil("prompt")
+ try_prompt
super
+ try_acr_values
end
# this executes before checking for a logged in account
def try_prompt
- prompt = param_or_nil("prompt")
+ return unless (prompt = param_or_nil("prompt"))
case prompt
when "none"
redirect_response_error("login_required") unless logged_in?
@@ -312,20 +348,50 @@
else
redirect_response_error("invalid_request")
end
end
- def create_oauth_grant(create_params = {})
- return super unless (nonce = param_or_nil("nonce"))
+ def try_acr_values
+ return unless (acr_values = param_or_nil("acr_values"))
- super(create_params.merge(oauth_grants_nonce_column => nonce))
+ acr_values.split(" ").each do |acr_value|
+ case acr_value
+ when "phr" then require_acr_value_phr
+ when "phrh" then require_acr_value_phrh
+ else
+ require_acr_value(acr_value)
+ end
+ end
end
+ def require_acr_value_phr
+ return unless respond_to?(:require_two_factor_authenticated)
+
+ require_two_factor_authenticated
+ end
+
+ def require_acr_value_phrh
+ require_acr_value_phr && two_factor_login_type_match?("webauthn")
+ end
+
+ def require_acr_value(_acr); end
+
+ def create_oauth_grant(create_params = {})
+ if (nonce = param_or_nil("nonce"))
+ create_params[oauth_grants_nonce_column] = nonce
+ end
+ if (acr = param_or_nil("acr"))
+ create_params[oauth_grants_acr_column] = acr
+ end
+ super
+ end
+
def create_oauth_token_from_authorization_code(oauth_grant, create_params)
- return super unless oauth_grant[oauth_grants_nonce_column]
+ create_params[oauth_tokens_nonce_column] = oauth_grant[oauth_grants_nonce_column] if oauth_grant[oauth_grants_nonce_column]
+ create_params[oauth_tokens_acr_column] = oauth_grant[oauth_grants_acr_column] if oauth_grant[oauth_grants_acr_column]
- super(oauth_grant, create_params.merge(oauth_tokens_nonce_column => oauth_grant[oauth_grants_nonce_column]))
+ super
end
def create_oauth_token(*)
oauth_token = super
generate_id_token(oauth_token)
@@ -336,15 +402,17 @@
oauth_scopes = oauth_token[oauth_tokens_scopes_column].split(oauth_scope_separator)
return unless oauth_scopes.include?("openid")
id_token_claims = jwt_claims(oauth_token)
+
id_token_claims[:nonce] = oauth_token[oauth_tokens_nonce_column] if oauth_token[oauth_tokens_nonce_column]
+ id_token_claims[:acr] = oauth_token[oauth_tokens_acr_column] if oauth_token[oauth_tokens_acr_column]
+
# Time when the End-User authentication occurred.
- #
- id_token_claims[:auth_time] = oauth_token[oauth_tokens_auth_time_column].to_i
+ id_token_claims[:auth_time] = last_account_login_at.to_i
account = db[accounts_table].where(account_id_column => oauth_token[oauth_tokens_account_id_column]).first
# this should never happen!
# a newly minted oauth token from a grant should have been assigned to an account
@@ -375,38 +443,70 @@
by_oidc[oidc] << param.to_sym if param
end
oidc_scopes, additional_scopes = scopes_by_claim.keys.partition { |key| OIDC_SCOPES_MAP.key?(key) }
+ if (claims_locales = param_or_nil("claims_locales"))
+ claims_locales = claims_locales.split(" ").map(&:to_sym)
+ end
+
unless oidc_scopes.empty?
if respond_to?(:get_oidc_param)
+ get_oidc_param = proxy_get_param(:get_oidc_param, claims, claims_locales)
+
oidc_scopes.each do |scope|
scope_claims = claims
params = scopes_by_claim[scope]
params = params.empty? ? OIDC_SCOPES_MAP[scope] : (OIDC_SCOPES_MAP[scope] & params)
scope_claims = (claims["address"] = {}) if scope == "address"
+
params.each do |param|
- scope_claims[param] = __send__(:get_oidc_param, account, param)
+ get_oidc_param[account, param, scope_claims]
end
end
else
warn "`get_oidc_param(account, claim)` must be implemented to use oidc scopes."
end
end
return if additional_scopes.empty?
if respond_to?(:get_additional_param)
+ get_additional_param = proxy_get_param(:get_additional_param, claims, claims_locales)
+
additional_scopes.each do |scope|
- claims[scope] = __send__(:get_additional_param, account, scope.to_sym)
+ get_additional_param[account, scope.to_sym]
end
else
warn "`get_additional_param(account, claim)` must be implemented to use oidc scopes."
end
end
+ def proxy_get_param(get_param_func, claims, claims_locales)
+ meth = method(get_param_func)
+ if meth.arity == 2
+ ->(account, param, cl = claims) { cl[param] = meth[account, param] }
+ elsif claims_locales.nil?
+ ->(account, param, cl = claims) { cl[param] = meth[account, param, nil] }
+ else
+ lambda do |account, param, cl = claims|
+ claims_values = claims_locales.map do |locale|
+ meth[account, param, locale]
+ end
+
+ if claims_values.uniq.size == 1
+ cl[param] = claims_values.first
+ else
+ claims_locales.zip(claims_values).each do |locale, value|
+ cl["#{param}##{locale}"] = value
+ end
+ end
+ end
+ end
+ end
+
def json_access_token_payload(oauth_token)
payload = super
payload["id_token"] = oauth_token[:id_token] if oauth_token[:id_token]
payload
end
@@ -450,9 +550,15 @@
create_params = {
oauth_tokens_account_id_column => account_id,
oauth_tokens_oauth_application_id_column => oauth_application[oauth_applications_id_column],
oauth_tokens_scopes_column => scopes
}
+ if (nonce = param_or_nil("nonce"))
+ create_params[oauth_grants_nonce_column] = nonce
+ end
+ if (acr = param_or_nil("acr"))
+ create_params[oauth_grants_acr_column] = acr
+ end
oauth_token = generate_oauth_token(create_params, false)
generate_id_token(oauth_token)
params = json_access_token_payload(oauth_token)
params.delete("access_token")
params