module Rodauth
Constants
- ACCESS_TYPES
- APPLICATION_REQUIRED_PARAMS
Application
- APPROVAL_PROMPTS
- JWKS
- OIDC_SCOPES_MAP
openid.net/specs/openid-connect-core-1_0.html#StandardClaims
- PROTECTED_APPLICATION_ATTRIBUTES
- REQUIRED_METADATA_KEYS
- SCOPES
- SERVER_METADATA
Resource server mode
- VALID_METADATA_KEYS
Public Instance Methods
_generate_oauth_token(params = {})
click to toggle source
# File lib/rodauth/features/oauth_base.rb, line 452 def _generate_oauth_token(params = {}) ds = db[oauth_tokens_table] if __one_oauth_token_per_account token = __insert_or_update_and_return__( ds, oauth_tokens_id_column, oauth_tokens_unique_columns, params, Sequel.expr(Sequel[oauth_tokens_table][oauth_tokens_expires_in_column]) > Sequel::CURRENT_TIMESTAMP, ([oauth_tokens_token_column, oauth_tokens_refresh_token_column] if oauth_reuse_access_token) ) # if the previous operation didn't return a row, it means that the conditions # invalidated the update, and the existing token is still valid. token || ds.where( oauth_tokens_account_id_column => params[oauth_tokens_account_id_column], oauth_tokens_oauth_application_id_column => params[oauth_tokens_oauth_application_id_column] ).first else if oauth_reuse_access_token unique_conds = Hash[oauth_tokens_unique_columns.map { |column| [column, params[column]] }] valid_token = ds.where(Sequel.expr(Sequel[oauth_tokens_table][oauth_tokens_expires_in_column]) > Sequel::CURRENT_TIMESTAMP) .where(unique_conds).first return valid_token if valid_token end __insert_and_return__(ds, oauth_tokens_id_column, params) end end
_json_response_body(hash)
click to toggle source
# File lib/rodauth/features/oauth_base.rb, line 711 def _json_response_body(hash) if request.respond_to?(:convert_to_json) request.send(:convert_to_json, hash) else JSON.dump(hash) end end
_jwt_key()
click to toggle source
# File lib/rodauth/features/oauth_jwt.rb, line 283 def _jwt_key @_jwt_key ||= oauth_jwt_key || begin if oauth_application if (jwks = oauth_application_jwks) jwks = JSON.parse(jwks, symbolize_names: true) if jwks && jwks.is_a?(String) jwks else oauth_application[oauth_applications_jwt_public_key_column] end end end end
_jwt_public_key()
click to toggle source
# File lib/rodauth/features/oauth_jwt.rb, line 297 def _jwt_public_key @_jwt_public_key ||= oauth_jwt_public_key || begin if oauth_application jwks || oauth_application[oauth_applications_jwt_public_key_column] else _jwt_key end end end
accepts_json?()
click to toggle source
# File lib/rodauth/features/oauth_base.rb, line 168 def accepts_json? return true if only_json? (accept = request.env["HTTP_ACCEPT"]) && accept =~ json_request_regexp end
account_from_bearer_assertion_subject(subject)
click to toggle source
# File lib/rodauth/features/oauth_assertion_base.rb, line 48 def account_from_bearer_assertion_subject(subject) __insert_or_do_nothing_and_return__( db[accounts_table], account_id_column, [login_column], login_column => subject ) end
account_from_jwt_bearer_assertion(assertion)
click to toggle source
# File lib/rodauth/features/oauth_jwt_bearer_grant.rb, line 37 def account_from_jwt_bearer_assertion(assertion) claims = jwt_assertion(assertion) return unless claims account_from_bearer_assertion_subject(claims["sub"]) end
account_from_saml2_bearer_assertion(assertion)
click to toggle source
# File lib/rodauth/features/oauth_saml_bearer_grant.rb, line 47 def account_from_saml2_bearer_assertion(assertion) saml = saml_assertion(assertion) return unless saml account_from_bearer_assertion_subject(saml.nameid) end
allow_cors(request)
click to toggle source
# File lib/rodauth/features/oidc.rb, line 529 def allow_cors(request) return unless request.request_method == "OPTIONS" response["Access-Control-Allow-Origin"] = "*" response["Access-Control-Allow-Methods"] = "GET, OPTIONS" response["Access-Control-Max-Age"] = "3600" response.status = 200 request.halt end
assertion_grant_type(grant_type = param("grant_type"))
click to toggle source
# File lib/rodauth/features/oauth_assertion_base.rb, line 88 def assertion_grant_type(grant_type = param("grant_type")) grant_type.delete_prefix("urn:ietf:params:oauth:grant-type:").tr("-", "_") end
assertion_grant_type?(grant_type = param("grant_type"))
click to toggle source
# File lib/rodauth/features/oauth_assertion_base.rb, line 80 def assertion_grant_type?(grant_type = param("grant_type")) grant_type.start_with?("urn:ietf:params:oauth:grant-type:") end
auth_server_jwks_set()
click to toggle source
Resource Server only!
returns the jwks set from the authorization server.
# File lib/rodauth/features/oauth_jwt.rb, line 310 def auth_server_jwks_set metadata = authorization_server_metadata return unless metadata && (jwks_uri = metadata[:jwks_uri]) jwks_uri = URI(jwks_uri) jwks = JWKS[jwks_uri] return jwks if jwks JWKS.set(jwks_uri) do http = Net::HTTP.new(jwks_uri.host, jwks_uri.port) http.use_ssl = jwks_uri.scheme == "https" request = Net::HTTP::Get.new(jwks_uri.request_uri) request["accept"] = json_response_content_type response = http.request(request) authorization_required unless response.code.to_i == 200 # time-to-live ttl = if response.key?("cache-control") cache_control = response["cache-control"] cache_control[/max-age=(\d+)/, 1].to_i elsif response.key?("expires") Time.parse(response["expires"]).to_i - Time.now.to_i end [JSON.parse(response.body, symbolize_names: true), ttl] end end
before_introspection_request(request)
click to toggle source
# File lib/rodauth/features/oauth_token_introspection.rb, line 99 def before_introspection_request(request); end
check_csrf?()
click to toggle source
Calls superclass method
# File lib/rodauth/features/oauth_application_management.rb, line 139 def check_csrf? case request.path when oauth_applications_path only_json? ? false : super else super end end
check_valid_access_type?()
click to toggle source
# File lib/rodauth/features/oauth_authorization_code_grant.rb, line 214 def check_valid_access_type? return true unless use_oauth_access_type? access_type = param_or_nil("access_type") !access_type || ACCESS_TYPES.include?(access_type) end
check_valid_approval_prompt?()
click to toggle source
# File lib/rodauth/features/oauth_authorization_code_grant.rb, line 223 def check_valid_approval_prompt? return true unless use_oauth_access_type? approval_prompt = param_or_nil("approval_prompt") !approval_prompt || APPROVAL_PROMPTS.include?(approval_prompt) end
check_valid_grant_challenge?(grant, verifier)
click to toggle source
# File lib/rodauth/features/oauth_pkce.rb, line 76 def check_valid_grant_challenge?(grant, verifier) challenge = grant[oauth_grants_code_challenge_column] case grant[oauth_grants_code_challenge_method_column] when "plain" challenge == verifier when "S256" generated_challenge = Base64.urlsafe_encode64(Digest::SHA256.digest(verifier)) generated_challenge.delete_suffix!("=") while generated_challenge.end_with?("=") challenge == generated_challenge else redirect_response_error("unsupported_transform_algorithm") end end
check_valid_redirect_uri?()
click to toggle source
# File lib/rodauth/features/oauth_authorization_code_grant.rb, line 236 def check_valid_redirect_uri? oauth_application[oauth_applications_redirect_uri_column].split(" ").include?(redirect_uri) end
check_valid_response_type?()
click to toggle source
# File lib/rodauth/features/oauth_authorization_code_grant.rb, line 230 def check_valid_response_type? response_type = param_or_nil("response_type") response_type.nil? || response_type == "code" end
check_valid_scopes?()
click to toggle source
# File lib/rodauth/features/oauth_base.rb, line 729 def check_valid_scopes? return false unless scopes (scopes - oauth_application[oauth_applications_scopes_column].split(oauth_scope_separator)).empty? end
check_valid_uri?(uri)
click to toggle source
# File lib/rodauth/features/oauth_base.rb, line 735 def check_valid_uri?(uri) URI::DEFAULT_PARSER.make_regexp(oauth_valid_uri_schemes).match?(uri) end
client_assertion_type(assertion_type = param("client_assertion_type"))
click to toggle source
# File lib/rodauth/features/oauth_assertion_base.rb, line 92 def client_assertion_type(assertion_type = param("client_assertion_type")) assertion_type.delete_prefix("urn:ietf:params:oauth:client-assertion-type:").tr("-", "_") end
client_assertion_type?(client_assertion_type = param("client_assertion_type"))
click to toggle source
# File lib/rodauth/features/oauth_assertion_base.rb, line 84 def client_assertion_type?(client_assertion_type = param("client_assertion_type")) client_assertion_type.start_with?("urn:ietf:params:oauth:client-assertion-type:") end
create_oauth_application()
click to toggle source
# File lib/rodauth/features/oauth_application_management.rb, line 189 def create_oauth_application create_params = { oauth_applications_account_id_column => account_id, oauth_applications_name_column => oauth_application_params[oauth_application_name_param], oauth_applications_description_column => oauth_application_params[oauth_application_description_param], oauth_applications_scopes_column => oauth_application_params[oauth_application_scopes_param], oauth_applications_homepage_url_column => oauth_application_params[oauth_application_homepage_url_param] } redirect_uris = oauth_application_params[oauth_application_redirect_uri_param] redirect_uris = redirect_uris.to_a.reject(&:empty?).join(" ") if redirect_uris.respond_to?(:each) create_params[oauth_applications_redirect_uri_column] = redirect_uris unless redirect_uris.empty? # set client ID/secret pairs create_params.merge! \ oauth_applications_client_secret_column => \ secret_hash(oauth_application_params[oauth_application_client_secret_param]) create_params[oauth_applications_scopes_column] = if create_params[oauth_applications_scopes_column] create_params[oauth_applications_scopes_column].join(oauth_scope_separator) else oauth_application_default_scope end rescue_from_uniqueness_error do create_params[oauth_applications_client_id_column] = oauth_unique_id_generator db[oauth_applications_table].insert(create_params) end end
create_oauth_grant(create_params = {})
click to toggle source
# File lib/rodauth/features/oauth_authorization_code_grant.rb, line 104 def create_oauth_grant(create_params = {}) create_params.merge!( oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column], oauth_grants_redirect_uri_column => redirect_uri, oauth_grants_expires_in_column => Sequel.date_add(Sequel::CURRENT_TIMESTAMP, seconds: oauth_grant_expires_in), oauth_grants_scopes_column => scopes.join(oauth_scope_separator) ) # Access Type flow if use_oauth_access_type? && (access_type = param_or_nil("access_type")) create_params[oauth_grants_access_type_column] = access_type end ds = db[oauth_grants_table] rescue_from_uniqueness_error do create_params[oauth_grants_code_column] = oauth_unique_id_generator __insert_and_return__(ds, oauth_grants_id_column, create_params) end create_params[oauth_grants_code_column] end
create_oauth_token(grant_type)
click to toggle source
Calls superclass method
# File lib/rodauth/features/oauth_assertion_base.rb, line 57 def create_oauth_token(grant_type) return super unless assertion_grant_type?(grant_type) && supported_grant_type?(grant_type) account = __send__(:"account_from_#{assertion_grant_type}_assertion", param("assertion")) redirect_response_error("invalid_grant") unless account grant_scopes = if param_or_nil("scope") redirect_response_error("invalid_grant") unless check_valid_scopes? scopes else @oauth_application[oauth_applications_scopes_column] end create_params = { oauth_tokens_account_id_column => account[account_id_column], oauth_tokens_oauth_application_id_column => @oauth_application[oauth_applications_id_column], oauth_tokens_scopes_column => grant_scopes } generate_oauth_token(create_params, false) end
create_oauth_token_from_token(oauth_token, update_params)
click to toggle source
# File lib/rodauth/features/oauth_base.rb, line 573 def create_oauth_token_from_token(oauth_token, update_params) redirect_response_error("invalid_grant") unless token_from_application?(oauth_token, oauth_application) rescue_from_uniqueness_error do oauth_tokens_ds = db[oauth_tokens_table] token = oauth_unique_id_generator if oauth_tokens_token_hash_column update_params[oauth_tokens_token_hash_column] = generate_token_hash(token) else update_params[oauth_tokens_token_column] = token end oauth_token = if oauth_refresh_token_protection_policy == "rotation" insert_params = { **update_params, oauth_tokens_oauth_token_id_column => oauth_token[oauth_tokens_id_column], oauth_tokens_scopes_column => oauth_token[oauth_tokens_scopes_column] } refresh_token = oauth_unique_id_generator if oauth_tokens_refresh_token_hash_column insert_params[oauth_tokens_refresh_token_hash_column] = generate_token_hash(refresh_token) else insert_params[oauth_tokens_refresh_token_column] = refresh_token end # revoke the refresh token oauth_tokens_ds.where(oauth_tokens_id_column => oauth_token[oauth_tokens_id_column]) .update(oauth_tokens_revoked_at_column => Sequel::CURRENT_TIMESTAMP) insert_params[oauth_tokens_oauth_token_id_column] = oauth_token[oauth_tokens_id_column] __insert_and_return__(oauth_tokens_ds, oauth_tokens_id_column, insert_params) else # includes none ds = oauth_tokens_ds.where(oauth_tokens_id_column => oauth_token[oauth_tokens_id_column]) __update_and_return__(ds, update_params) end oauth_token[oauth_tokens_token_column] = token oauth_token[oauth_tokens_refresh_token_column] = refresh_token if refresh_token oauth_token end end
do_register(return_params = request.params.dup)
click to toggle source
# File lib/rodauth/features/oauth_dynamic_client_registration.rb, line 169 def do_register(return_params = request.params.dup) # set defaults create_params = @oauth_application_params create_params[oauth_applications_scopes_column] ||= return_params["scopes"] = oauth_application_default_scope.join(" ") create_params[oauth_applications_token_endpoint_auth_method_column] ||= begin return_params["token_endpoint_auth_method"] = "client_secret_basic" "client_secret_basic" end create_params[oauth_applications_grant_types_column] ||= begin return_params["grant_types"] = %w[authorization_code] "authorization_code" end create_params[oauth_applications_response_types_column] ||= begin return_params["response_types"] = %w[code] "code" end rescue_from_uniqueness_error do client_id = oauth_unique_id_generator create_params[oauth_applications_client_id_column] = client_id return_params["client_id"] = client_id return_params["client_id_issued_at"] = Time.now.utc.iso8601 if create_params.key?(oauth_applications_client_secret_column) create_params[oauth_applications_client_secret_column] = secret_hash(create_params[oauth_applications_client_secret_column]) return_params.delete("client_secret") else client_secret = oauth_unique_id_generator create_params[oauth_applications_client_secret_column] = secret_hash(client_secret) return_params["client_secret"] = client_secret return_params["client_secret_expires_at"] = 0 end db[oauth_applications_table].insert(create_params) end return_params end
fetch_access_token()
click to toggle source
# File lib/rodauth/features/oauth_base.rb, line 216 def fetch_access_token value = request.env["HTTP_AUTHORIZATION"] return unless value && !value.empty? scheme, token = value.split(" ", 2) return unless scheme.downcase == oauth_token_type return if token.nil? || token.empty? token end
fill_with_account_claims(claims, account, scopes)
click to toggle source
aka fill_with_standard_claims
# File lib/rodauth/features/oidc.rb, line 366 def fill_with_account_claims(claims, account, scopes) scopes_by_claim = scopes.each_with_object({}) do |scope, by_oidc| next if scope == "openid" oidc, param = scope.split(".", 2) by_oidc[oidc] ||= [] by_oidc[oidc] << param.to_sym if param end oidc_scopes, additional_scopes = scopes_by_claim.keys.partition { |key| OIDC_SCOPES_MAP.key?(key) } unless oidc_scopes.empty? if respond_to?(:get_oidc_param) 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) 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) additional_scopes.each do |scope| claims[scope] = __send__(:get_additional_param, account, scope.to_sym) end else warn "`get_additional_param(account, claim)` must be implemented to use oidc scopes." end end
generate_id_token(oauth_token)
click to toggle source
# File lib/rodauth/features/oidc.rb, line 334 def generate_id_token(oauth_token) 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] # Time when the End-User authentication occurred. # # Sounds like the same as issued at claim. id_token_claims[:auth_time] = id_token_claims[:iat] 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 # who just authorized its generation. return unless account fill_with_account_claims(id_token_claims, account, oauth_scopes) params = { jwks: oauth_application_jwks, signing_algorithm: oauth_application[oauth_applications_id_token_signed_response_alg_column] || oauth_jwt_algorithm, encryption_algorithm: oauth_application[oauth_applications_id_token_encrypted_response_alg_column], encryption_method: oauth_application[oauth_applications_id_token_encrypted_response_enc_column] } oauth_token[:id_token] = jwt_encode(id_token_claims, **params) end
generate_jti(payload)
click to toggle source
# File lib/rodauth/features/oauth_jwt.rb, line 342 def generate_jti(payload) # Use the key and iat to create a unique key per request to prevent replay attacks jti_raw = [ payload[:aud] || payload["aud"], payload[:iat] || payload["iat"] ].join(":").to_s Digest::SHA256.hexdigest(jti_raw) end
generate_oauth_token(params = {}, should_generate_refresh_token = true)
click to toggle source
# File lib/rodauth/features/oauth_base.rb, line 421 def generate_oauth_token(params = {}, should_generate_refresh_token = true) create_params = { oauth_tokens_expires_in_column => Sequel.date_add(Sequel::CURRENT_TIMESTAMP, seconds: oauth_token_expires_in) }.merge(params) rescue_from_uniqueness_error do token = oauth_unique_id_generator if oauth_tokens_token_hash_column create_params[oauth_tokens_token_hash_column] = generate_token_hash(token) else create_params[oauth_tokens_token_column] = token end refresh_token = nil if should_generate_refresh_token refresh_token = oauth_unique_id_generator if oauth_tokens_refresh_token_hash_column create_params[oauth_tokens_refresh_token_hash_column] = generate_token_hash(refresh_token) else create_params[oauth_tokens_refresh_token_column] = refresh_token end end oauth_token = _generate_oauth_token(create_params) oauth_token[oauth_tokens_token_column] = token oauth_token[oauth_tokens_refresh_token_column] = refresh_token if refresh_token oauth_token end end
generate_token_hash(token)
click to toggle source
# File lib/rodauth/features/oauth_base.rb, line 405 def generate_token_hash(token) Base64.urlsafe_encode64(Digest::SHA256.digest(token)) end
generate_user_code()
click to toggle source
# File lib/rodauth/features/oauth_device_grant.rb, line 119 def generate_user_code user_code_size = oauth_device_code_grant_user_code_size SecureRandom.random_number(36**user_code_size) .to_s(36) # 0 to 9, a to z .upcase .rjust(user_code_size, "0") end
introspection_request(token_type_hint, token)
click to toggle source
# File lib/rodauth/features/oauth_token_introspection.rb, line 82 def introspection_request(token_type_hint, token) auth_url = URI(authorization_server_url) http = Net::HTTP.new(auth_url.host, auth_url.port) http.use_ssl = auth_url.scheme == "https" request = Net::HTTP::Post.new(introspect_path) request["content-type"] = "application/x-www-form-urlencoded" request["accept"] = json_response_content_type request.set_form_data({ "token_type_hint" => token_type_hint, "token" => token }) before_introspection_request(request) response = http.request(request) authorization_required unless response.code.to_i == 200 JSON.parse(response.body) end
issuer()
click to toggle source
# File lib/rodauth/features/oauth_jwt.rb, line 104 def issuer @issuer ||= oauth_jwt_token_issuer || authorization_server_url end
json_access_token_payload(oauth_token)
click to toggle source
# File lib/rodauth/features/oauth_base.rb, line 520 def json_access_token_payload(oauth_token) payload = { "access_token" => oauth_token[oauth_tokens_token_column], "token_type" => oauth_token_type, "expires_in" => oauth_token_expires_in } payload["refresh_token"] = oauth_token[oauth_tokens_refresh_token_column] if oauth_token[oauth_tokens_refresh_token_column] payload end
json_request?()
click to toggle source
copied from the jwt feature
# File lib/rodauth/features/oauth_base.rb, line 176 def json_request? return @json_request if defined?(@json_request) @json_request = request.content_type =~ json_request_regexp end
json_response_success(body, cache = false)
click to toggle source
# File lib/rodauth/features/oauth_base.rb, line 678 def json_response_success(body, cache = false) response.status = 200 response["Content-Type"] ||= json_response_content_type if cache # defaulting to 1-day for everyone, for now at least max_age = 60 * 60 * 24 response["Cache-Control"] = "private, max-age=#{max_age}" else response["Cache-Control"] = "no-store" response["Pragma"] = "no-cache" end json_payload = _json_response_body(body) response.write(json_payload) request.halt end
json_token_introspect_payload(oauth_token)
click to toggle source
Calls superclass method
# File lib/rodauth/features/oauth_jwt.rb, line 254 def json_token_introspect_payload(oauth_token) return { active: false } unless oauth_token return super unless oauth_token["sub"] # naive check on whether it's a jwt token { active: true, scope: oauth_token["scope"], client_id: oauth_token["client_id"], # username token_type: "access_token", exp: oauth_token["exp"], iat: oauth_token["iat"], nbf: oauth_token["nbf"], sub: oauth_token["sub"], aud: oauth_token["aud"], iss: oauth_token["iss"], jti: oauth_token["jti"] } end
jwk_import(data)
click to toggle source
# File lib/rodauth/features/oauth_jwt.rb, line 414 def jwk_import(data) JSON::JWK.new(data) end
jwks_set()
click to toggle source
# File lib/rodauth/features/oauth_jwt.rb, line 498 def jwks_set @jwks_set ||= [ (JSON::JWK.new(oauth_jwt_public_key).merge(use: "sig", alg: oauth_jwt_algorithm) if oauth_jwt_public_key), (JSON::JWK.new(oauth_jwt_legacy_public_key).merge(use: "sig", alg: oauth_jwt_legacy_algorithm) if oauth_jwt_legacy_public_key), (JSON::JWK.new(oauth_jwt_jwe_public_key).merge(use: "enc", alg: oauth_jwt_jwe_algorithm) if oauth_jwt_jwe_public_key) ].compact end
jwt_assertion(assertion)
click to toggle source
# File lib/rodauth/features/oauth_jwt_bearer_grant.rb, line 45 def jwt_assertion(assertion) claims = jwt_decode(assertion, verify_iss: false, verify_aud: false) return unless verify_aud(token_url, claims["aud"]) claims end
jwt_claims(oauth_token)
click to toggle source
# File lib/rodauth/features/oauth_jwt.rb, line 205 def jwt_claims(oauth_token) issued_at = Time.now.to_i claims = { iss: issuer, # issuer iat: issued_at, # issued at # # sub REQUIRED - as defined in section 4.1.2 of [RFC7519]. In case of # access tokens obtained through grants where a resource owner is # involved, such as the authorization code grant, the value of "sub" # SHOULD correspond to the subject identifier of the resource owner. # In case of access tokens obtained through grants where no resource # owner is involved, such as the client credentials grant, the value # of "sub" SHOULD correspond to an identifier the authorization # server uses to indicate the client application. sub: jwt_subject(oauth_token), client_id: oauth_application[oauth_applications_client_id_column], exp: issued_at + oauth_token_expires_in, aud: (oauth_jwt_audience || oauth_application[oauth_applications_client_id_column]) } claims[:auth_time] = last_account_login_at.to_i if last_account_login_at claims end
jwt_decode( token, jwks: nil, jws_key: oauth_jwt_public_key || _jwt_key, jws_algorithm: oauth_jwt_algorithm, jwe_key: oauth_jwt_jwe_key, jws_encryption_algorithm: oauth_jwt_jwe_algorithm, jws_encryption_method: oauth_jwt_jwe_encryption_method, verify_claims: true, verify_jti: true, verify_iss: true, verify_aud: false, ** )
click to toggle source
# File lib/rodauth/features/oauth_jwt.rb, line 448 def jwt_decode( token, jwks: nil, jws_key: oauth_jwt_public_key || _jwt_key, jws_algorithm: oauth_jwt_algorithm, jwe_key: oauth_jwt_jwe_key, jws_encryption_algorithm: oauth_jwt_jwe_algorithm, jws_encryption_method: oauth_jwt_jwe_encryption_method, verify_claims: true, verify_jti: true, verify_iss: true, verify_aud: false, ** ) token = JSON::JWT.decode(token, oauth_jwt_jwe_key).plain_text if jwe_key claims = if is_authorization_server? if oauth_jwt_legacy_public_key JSON::JWT.decode(token, JSON::JWK::Set.new({ keys: jwks_set })) elsif jwks enc_algs = [jws_encryption_algorithm].compact enc_meths = [jws_encryption_method].compact sig_algs = [jws_algorithm].compact.map(&:to_sym) jws = JSON::JWT.decode(token, JSON::JWK::Set.new({ keys: jwks }), enc_algs + sig_algs, enc_meths) jws = JSON::JWT.decode(jws.plain_text, JSON::JWK::Set.new({ keys: jwks }), sig_algs) if jws.is_a?(JSON::JWE) jws elsif jws_key JSON::JWT.decode(token, jws_key) end elsif (jwks = auth_server_jwks_set) JSON::JWT.decode(token, JSON::JWK::Set.new(jwks)) end now = Time.now if verify_claims && ( (!claims[:exp] || Time.at(claims[:exp]) < now) && (claims[:nbf] && Time.at(claims[:nbf]) < now) && (claims[:iat] && Time.at(claims[:iat]) < now) && (verify_iss && claims[:iss] != issuer) && (verify_aud && !verify_aud(claims[:aud], claims[:client_id])) && (verify_jti && !verify_jti(claims[:jti], claims)) ) return end claims rescue JSON::JWT::Exception nil end
jwt_decode_with_jwe( token, jwks: nil, jwe_key: oauth_jwt_jwe_key, jws_encryption_algorithm: oauth_jwt_jwe_algorithm, jws_encryption_method: oauth_jwt_jwe_encryption_method, **args )
click to toggle source
# File lib/rodauth/features/oauth_jwt.rb, line 641 def jwt_decode_with_jwe( token, jwks: nil, jwe_key: oauth_jwt_jwe_key, jws_encryption_algorithm: oauth_jwt_jwe_algorithm, jws_encryption_method: oauth_jwt_jwe_encryption_method, **args ) token = if jwks && jwks.any? { |k| k[:use] == "enc" } JWE.__rodauth_oauth_decrypt_from_jwks(token, jwks, alg: jws_encryption_algorithm, enc: jws_encryption_method) elsif jwe_key JWE.decrypt(token, jwe_key) else token end jwt_decode_without_jwe(token, jwks: jwks, **args) rescue JWE::DecodeError => e jwt_decode_without_jwe(token, jwks: jwks, **args) if e.message.include?("Not enough or too many segments") end
jwt_encode(payload, jwks: nil, jwe_key: oauth_jwt_jwe_public_key || oauth_jwt_jwe_key, signing_algorithm: oauth_jwt_algorithm, encryption_algorithm: oauth_jwt_jwe_algorithm, encryption_method: oauth_jwt_jwe_encryption_method)
click to toggle source
# File lib/rodauth/features/oauth_jwt.rb, line 418 def jwt_encode(payload, jwks: nil, jwe_key: oauth_jwt_jwe_public_key || oauth_jwt_jwe_key, signing_algorithm: oauth_jwt_algorithm, encryption_algorithm: oauth_jwt_jwe_algorithm, encryption_method: oauth_jwt_jwe_encryption_method) payload[:jti] = generate_jti(payload) jwt = JSON::JWT.new(payload) key = oauth_jwt_keys[signing_algorithm] || _jwt_key key = key.first if key.is_a?(Array) jwk = JSON::JWK.new(key || "") jwt = jwt.sign(jwk, signing_algorithm) jwt.kid = jwk.thumbprint if jwks && (jwk = jwks.find { |k| k[:use] == "enc" && k[:alg] == encryption_algorithm && k[:enc] == encryption_method }) jwk = JSON::JWK.new(jwk) jwe = jwt.encrypt(jwk, encryption_algorithm.to_sym, encryption_method.to_sym) jwe.to_s elsif jwe_key algorithm = encryption_algorithm.to_sym if encryption_algorithm meth = encryption_method.to_sym if encryption_method jwt.encrypt(jwe_key, algorithm, meth) else jwt.to_s end end
jwt_encode_with_jwe( payload, jwks: nil, jwe_key: oauth_jwt_jwe_public_key || oauth_jwt_jwe_key, encryption_algorithm: oauth_jwt_jwe_algorithm, encryption_method: oauth_jwt_jwe_encryption_method, **args )
click to toggle source
# File lib/rodauth/features/oauth_jwt.rb, line 555 def jwt_encode_with_jwe( payload, jwks: nil, jwe_key: oauth_jwt_jwe_public_key || oauth_jwt_jwe_key, encryption_algorithm: oauth_jwt_jwe_algorithm, encryption_method: oauth_jwt_jwe_encryption_method, **args ) token = jwt_encode_without_jwe(payload, **args) return token unless encryption_algorithm && encryption_method if jwks && jwks.any? { |k| k[:use] == "enc" } JWE.__rodauth_oauth_encrypt_from_jwks(token, jwks, alg: encryption_algorithm, enc: encryption_method) elsif jwe_key params = { zip: "DEF", copyright: oauth_jwt_jwe_copyright } params[:enc] = encryption_method if encryption_method params[:alg] = encryption_algorithm if encryption_algorithm JWE.encrypt(token, jwe_key, **params) else token end end
jwt_response_success(jwt, cache = false)
click to toggle source
# File lib/rodauth/features/oauth_jwt.rb, line 706 def jwt_response_success(jwt, cache = false) response.status = 200 response["Content-Type"] ||= "application/jwt" if cache # defaulting to 1-day for everyone, for now at least max_age = 60 * 60 * 24 response["Cache-Control"] = "private, max-age=#{max_age}" else response["Cache-Control"] = "no-store" response["Pragma"] = "no-cache" end response.write(jwt) request.halt end
jwt_subject(oauth_token)
click to toggle source
# File lib/rodauth/features/oauth_jwt.rb, line 232 def jwt_subject(oauth_token) subject_type = if oauth_application oauth_application[oauth_applications_subject_type_column] || oauth_jwt_subject_type else oauth_jwt_subject_type end case subject_type when "public" oauth_token[oauth_tokens_account_id_column] when "pairwise" id = oauth_token[oauth_tokens_account_id_column] application_id = oauth_token[oauth_tokens_oauth_application_id_column] Digest::SHA256.hexdigest("#{id}#{application_id}#{oauth_jwt_subject_secret}") else raise StandardError, "unexpected subject (#{subject_type})" end end
last_account_login_at()
click to toggle source
# File lib/rodauth/features/oauth_jwt.rb, line 99 def last_account_login_at nil end
mac_signature_matches?(oauth_token, mac_attributes)
click to toggle source
# File lib/rodauth/features/oauth_http_mac.rb, line 52 def mac_signature_matches?(oauth_token, mac_attributes) nonce = mac_attributes["nonce"] uri = URI(request.url) request_signature = [ nonce, request.request_method, uri.request_uri, uri.host, uri.port ].join("\n") + ("\n" * 3) mac_algorithm = case oauth_mac_algorithm when "hmac-sha-256" OpenSSL::Digest::SHA256 when "hmac-sha-1" OpenSSL::Digest::SHA1 else raise ArgumentError, "Unsupported algorithm" end mac_signature = Base64.strict_encode64 \ OpenSSL::HMAC.digest(mac_algorithm.new, oauth_token[oauth_tokens_mac_key_column], request_signature) mac_signature == mac_attributes["mac"] end
no_auth_oauth_application?(_oauth_application)
click to toggle source
# File lib/rodauth/features/oauth_base.rb, line 375 def no_auth_oauth_application?(_oauth_application) supported_auth_methods.include?("none") end
oauth_application()
click to toggle source
# File lib/rodauth/features/oauth_base.rb, line 204 def oauth_application return @oauth_application if defined?(@oauth_application) @oauth_application = begin client_id = param_or_nil("client_id") return unless client_id db[oauth_applications_table].filter(oauth_applications_client_id_column => client_id).first end end
oauth_application_jwks()
click to toggle source
# File lib/rodauth/features/oauth_jwt.rb, line 359 def oauth_application_jwks jwks = oauth_application[oauth_applications_jwks_column] if jwks jwks = JSON.parse(jwks, symbolize_names: true) if jwks.is_a?(String) return jwks end jwks_uri = oauth_application[oauth_applications_jwks_uri_column] return unless jwks_uri jwks_uri = URI(jwks_uri) jwks = JWKS[jwks_uri] return jwks if jwks JWKS.set(jwks_uri) do http = Net::HTTP.new(jwks_uri.host, jwks_uri.port) http.use_ssl = jwks_uri.scheme == "https" request = Net::HTTP::Get.new(jwks_uri.request_uri) request["accept"] = json_response_content_type response = http.request(request) return unless response.code.to_i == 200 # time-to-live ttl = if response.key?("cache-control") cache_control = response["cache-control"] cache_control[/max-age=(\d+)/, 1].to_i elsif response.key?("expires") Time.parse(response["expires"]).to_i - Time.now.to_i end [JSON.parse(response.body, symbolize_names: true), ttl] end end
oauth_application_params()
click to toggle source
# File lib/rodauth/features/oauth_application_management.rb, line 150 def oauth_application_params @oauth_application_params ||= oauth_application_required_params.each_with_object({}) do |param, params| value = request.params[__send__(:"oauth_application_#{param}_param")] if value && !value.empty? params[param] = value else set_field_error(param, null_error_message) end end end
oauth_application_path(id)
click to toggle source
# File lib/rodauth/features/oauth_application_management.rb, line 69 def oauth_application_path(id) "#{oauth_applications_path}/#{id}" end
oauth_applications()
click to toggle source
/oauth-applications routes
# File lib/rodauth/features/oauth_application_management.rb, line 74 def oauth_applications request.on(oauth_applications_route) do require_account request.get "new" do new_oauth_application_view end request.on(oauth_applications_id_pattern) do |id| oauth_application = db[oauth_applications_table] .where(oauth_applications_id_column => id) .where(oauth_applications_account_id_column => account_id) .first next unless oauth_application scope.instance_variable_set(:@oauth_application, oauth_application) request.is do request.get do oauth_application_view end end request.on(oauth_applications_oauth_tokens_path) do page = Integer(param_or_nil("page") || 1) per_page = per_page_param(oauth_tokens_per_page) oauth_tokens = db[oauth_tokens_table] .where(oauth_tokens_oauth_application_id_column => id) .order(Sequel.desc(oauth_tokens_id_column)) scope.instance_variable_set(:@oauth_tokens, oauth_tokens.paginate(page, per_page)) request.get do oauth_application_oauth_tokens_view end end end request.get do page = Integer(param_or_nil("page") || 1) per_page = per_page_param(oauth_applications_per_page) scope.instance_variable_set(:@oauth_applications, db[oauth_applications_table] .where(oauth_applications_account_id_column => account_id) .order(Sequel.desc(oauth_applications_id_column)) .paginate(page, per_page)) oauth_applications_view end request.post do catch_error do validate_oauth_application_params transaction do before_create_oauth_application id = create_oauth_application after_create_oauth_application set_notice_flash create_oauth_application_notice_flash redirect "#{request.path}/#{id}" end end set_error_flash create_oauth_application_error_flash new_oauth_application_view end end end
oauth_applications_path(opts = {})
click to toggle source
# File lib/rodauth/features/oauth_application_management.rb, line 58 def oauth_applications_path(opts = {}) route_path(oauth_applications_route, opts) end
oauth_applications_url(opts = {})
click to toggle source
# File lib/rodauth/features/oauth_application_management.rb, line 62 def oauth_applications_url(opts = {}) route_url(oauth_applications_route, opts) end
oauth_jwt_jwe_algorithms_supported()
click to toggle source
# File lib/rodauth/features/oauth_jwt.rb, line 523 def oauth_jwt_jwe_algorithms_supported JWE::VALID_ALG end
oauth_jwt_jwe_encryption_methods_supported()
click to toggle source
# File lib/rodauth/features/oauth_jwt.rb, line 527 def oauth_jwt_jwe_encryption_methods_supported JWE::VALID_ENC end
oauth_management_pagination_link(page, label: page, current: false, classes: "")
click to toggle source
# File lib/rodauth/features/oauth_management_base.rb, line 20 def oauth_management_pagination_link(page, label: page, current: false, classes: "") classes += " disabled" if current || !page classes += " active" if current if page params = request.GET.merge("page" => page).map do |k, v| v ? "#{CGI.escape(String(k))}=#{CGI.escape(String(v))}" : CGI.escape(String(k)) end.join("&") href = "#{request.path}?#{params}" <<-HTML <li class="page-item #{classes}" #{'aria-current="page"' if current}> <a class="page-link" href="#{href}" tabindex="-1" aria-disabled="#{current || !page}"> #{label} </a> </li> HTML else <<-HTML <li class="page-item #{classes}"> <span class="page-link"> #{label} #{'<span class="sr-only">(current)</span>' if current} </span> </li> HTML end end
oauth_management_pagination_links(paginated_ds)
click to toggle source
# File lib/rodauth/features/oauth_management_base.rb, line 10 def oauth_management_pagination_links(paginated_ds) html = +'<nav aria-label="Pagination"><ul class="pagination">' html << oauth_management_pagination_link(paginated_ds.prev_page, label: oauth_management_pagination_previous_button) html << oauth_management_pagination_link(paginated_ds.current_page - 1) unless paginated_ds.first_page? html << oauth_management_pagination_link(paginated_ds.current_page, label: paginated_ds.current_page, current: true) html << oauth_management_pagination_link(paginated_ds.current_page + 1) unless paginated_ds.last_page? html << oauth_management_pagination_link(paginated_ds.next_page, label: oauth_management_pagination_next_button) html << "</ul></nav>" end
oauth_server_metadata(issuer = nil)
click to toggle source
# File lib/rodauth/features/oauth_base.rb, line 140 def oauth_server_metadata(issuer = nil) request.on(".well-known") do request.on("oauth-authorization-server") do request.get do json_response_success(oauth_server_metadata_body(issuer), true) end end end end
oauth_server_metadata_body(*)
click to toggle source
Calls superclass method
# File lib/rodauth/features/oauth_application_management.rb, line 219 def oauth_server_metadata_body(*) super.tap do |data| data[:registration_endpoint] = oauth_applications_url end end
oauth_token_by_refresh_token(token, revoked: false)
click to toggle source
# File lib/rodauth/features/oauth_base.rb, line 500 def oauth_token_by_refresh_token(token, revoked: false) ds = db[oauth_tokens_table] # # filter expired refresh tokens out. # an expired refresh token is a token whose access token expired for a period longer than the # refresh token expiration period. # ds = ds.where(Sequel.date_add(oauth_tokens_expires_in_column, seconds: oauth_refresh_token_expires_in) >= Sequel::CURRENT_TIMESTAMP) ds = if oauth_tokens_refresh_token_hash_column ds.where(oauth_tokens_refresh_token_hash_column => generate_token_hash(token)) else ds.where(oauth_tokens_refresh_token_column => token) end ds = ds.where(oauth_tokens_revoked_at_column => nil) unless revoked ds.first end
oauth_token_by_token(token)
click to toggle source
# File lib/rodauth/features/oauth_base.rb, line 496 def oauth_token_by_token(token) oauth_token_by_token_ds(token).first end
oauth_token_by_token_ds(token)
click to toggle source
# File lib/rodauth/features/oauth_base.rb, line 483 def oauth_token_by_token_ds(token) ds = db[oauth_tokens_table] ds = if oauth_tokens_token_hash_column ds.where(Sequel[oauth_tokens_table][oauth_tokens_token_hash_column] => generate_token_hash(token)) else ds.where(Sequel[oauth_tokens_table][oauth_tokens_token_column] => token) end ds.where(Sequel[oauth_tokens_table][oauth_tokens_expires_in_column] >= Sequel::CURRENT_TIMESTAMP) .where(Sequel[oauth_tokens_table][oauth_tokens_revoked_at_column] => nil) end
oauth_token_path(id)
click to toggle source
# File lib/rodauth/features/oauth_token_management.rb, line 35 def oauth_token_path(id) "#{oauth_tokens_path}/#{id}" end
oauth_tokens()
click to toggle source
# File lib/rodauth/features/oauth_token_management.rb, line 39 def oauth_tokens request.on(oauth_tokens_route) do require_account request.get do page = Integer(param_or_nil("page") || 1) per_page = per_page_param(oauth_tokens_per_page) scope.instance_variable_set(:@oauth_tokens, db[oauth_tokens_table] .select(Sequel[oauth_tokens_table].*, Sequel[oauth_applications_table][oauth_applications_name_column]) .join(oauth_applications_table, Sequel[oauth_tokens_table][oauth_tokens_oauth_application_id_column] => Sequel[oauth_applications_table][oauth_applications_id_column]) .where(Sequel[oauth_tokens_table][oauth_tokens_account_id_column] => account_id) .where(oauth_tokens_revoked_at_column => nil) .order(Sequel.desc(oauth_tokens_id_column)) .paginate(page, per_page)) oauth_tokens_view end request.post(oauth_tokens_id_pattern) do |id| db[oauth_tokens_table] .where(oauth_tokens_id_column => id) .where(oauth_tokens_account_id_column => account_id) .update(oauth_tokens_revoked_at_column => Sequel::CURRENT_TIMESTAMP) set_notice_flash revoke_oauth_token_notice_flash redirect oauth_tokens_path || "/" end end end
oauth_tokens_path(opts = {})
click to toggle source
# File lib/rodauth/features/oauth_token_management.rb, line 27 def oauth_tokens_path(opts = {}) route_path(oauth_tokens_route, opts) end
oauth_tokens_unique_columns()
click to toggle source
OAuth
Token Unique/Reuse
# File lib/rodauth/features/oauth_base.rb, line 313 def oauth_tokens_unique_columns [ oauth_tokens_oauth_application_id_column, oauth_tokens_account_id_column, oauth_tokens_scopes_column ] end
oauth_tokens_url(opts = {})
click to toggle source
# File lib/rodauth/features/oauth_token_management.rb, line 31 def oauth_tokens_url(opts = {}) route_url(oauth_tokens_route, opts) end
oauth_unique_id_generator()
click to toggle source
# File lib/rodauth/features/oauth_base.rb, line 401 def oauth_unique_id_generator SecureRandom.urlsafe_base64(32) end
openid_configuration(alt_issuer = nil)
click to toggle source
# File lib/rodauth/features/oidc.rb, line 210 def openid_configuration(alt_issuer = nil) request.on(".well-known/openid-configuration") do allow_cors(request) request.get do json_response_success(openid_configuration_body(alt_issuer), cache: true) end end end
openid_configuration_body(path = nil)
click to toggle source
Metadata
# File lib/rodauth/features/oidc.rb, line 475 def openid_configuration_body(path = nil) 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 else oidc_claims = OIDC_SCOPES_MAP[oidc] claims.concat(oidc_claims) if oidc_claims end end scope_claims.unshift("auth_time") if last_account_login_at response_types_supported = metadata[:response_types_supported] if metadata[:grant_types_supported].include?("implicit") response_types_supported += ["none", "id_token", "code token", "code id_token", "id_token token", "code id_token token"] end metadata.merge( userinfo_endpoint: userinfo_url, end_session_endpoint: (oidc_logout_url if use_rp_initiated_logout?), response_types_supported: response_types_supported, 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, 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: [], # 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
password_hash(password)
click to toggle source
From login_requirements_base feature
# File lib/rodauth/features/oauth_base.rb, line 416 def password_hash(password) BCrypt::Password.create(password, cost: BCrypt::Engine::DEFAULT_COST) end
per_page_param(default_per_page)
click to toggle source
# File lib/rodauth/features/oauth_management_base.rb, line 56 def per_page_param(default_per_page) per_page = param_or_nil("per_page") return default_per_page unless per_page per_page = per_page.to_i return default_per_page if per_page <= 0 [per_page, default_per_page].min end
post_configure()
click to toggle source
Calls superclass method
# File lib/rodauth/features/oauth_base.rb, line 275 def post_configure super # all of the extensions below involve DB changes. Resource server mode doesn't use # database functions for OAuth though. return unless is_authorization_server? self.class.__send__(:include, Rodauth::OAuth::ExtendDatabase(db)) # Check whether we can reutilize db entries for the same account / application pair one_oauth_token_per_account = db.indexes(oauth_tokens_table).values.any? do |definition| definition[:unique] && definition[:columns] == oauth_tokens_unique_columns end self.class.send(:define_method, :__one_oauth_token_per_account) { one_oauth_token_per_account } i18n_register(File.expand_path(File.join(__dir__, "..", "..", "..", "locales"))) if features.include?(:i18n) end
redirect_response_error(error_code, redirect_url = redirect_uri || request.referer || default_redirect)
click to toggle source
# File lib/rodauth/features/oauth_base.rb, line 648 def redirect_response_error(error_code, redirect_url = redirect_uri || request.referer || default_redirect) if accepts_json? status_code = if respond_to?(:"#{error_code}_response_status") send(:"#{error_code}_response_status") else invalid_oauth_response_status end throw_json_response_error(status_code, error_code) else redirect_url = URI.parse(redirect_url) query_params = [] query_params << if respond_to?(:"#{error_code}_error_code") "error=#{send(:"#{error_code}_error_code")}" else "error=#{error_code}" end if respond_to?(:"#{error_code}_message") message = send(:"#{error_code}_message") query_params << ["error_description=#{CGI.escape(message)}"] end query_params << redirect_url.query if redirect_url.query redirect_url.query = query_params.join("&") redirect(redirect_url.to_s) end end
redirect_uri()
click to toggle source
# File lib/rodauth/features/oauth_base.rb, line 195 def redirect_uri param_or_nil("redirect_uri") || begin return unless oauth_application redirect_uris = oauth_application[oauth_applications_redirect_uri_column].split(" ") redirect_uris.size == 1 ? redirect_uris.first : nil end end
register_invalid_application_type_message(application_type)
click to toggle source
# File lib/rodauth/features/oidc_dynamic_client_registration.rb, line 143 def register_invalid_application_type_message(application_type) "The application type '#{application_type}' is not allowed." end
register_invalid_contacts_message(contacts)
click to toggle source
# File lib/rodauth/features/oauth_dynamic_client_registration.rb, line 217 def register_invalid_contacts_message(contacts) "The contacts '#{contacts}' are not allowed by this server." end
register_invalid_grant_type_message(grant_type)
click to toggle source
# File lib/rodauth/features/oauth_dynamic_client_registration.rb, line 233 def register_invalid_grant_type_message(grant_type) "The grant type #{grant_type} is not allowed by this server." end
register_invalid_jwks_param_message(key1, key2)
click to toggle source
# File lib/rodauth/features/oauth_dynamic_client_registration.rb, line 225 def register_invalid_jwks_param_message(key1, key2) "The param '#{key1}' cannot be accepted together with param '#{key2}'." end
register_invalid_param_message(key)
click to toggle source
# File lib/rodauth/features/oauth_dynamic_client_registration.rb, line 213 def register_invalid_param_message(key) "The param '#{key}' is not supported by this server." end
register_invalid_response_type_for_grant_type_message(response_type, grant_type)
click to toggle source
# File lib/rodauth/features/oauth_dynamic_client_registration.rb, line 241 def register_invalid_response_type_for_grant_type_message(response_type, grant_type) "The grant type '#{grant_type}' must be registered for the response " \ "type '#{response_type}' to be allowed." end
register_invalid_response_type_message(response_type)
click to toggle source
# File lib/rodauth/features/oauth_dynamic_client_registration.rb, line 237 def register_invalid_response_type_message(response_type) "The response type #{response_type} is not allowed by this server." end
register_invalid_scopes_message(scopes)
click to toggle source
# File lib/rodauth/features/oauth_dynamic_client_registration.rb, line 229 def register_invalid_scopes_message(scopes) "The given scopes (#{scopes}) are not allowed by this server." end
register_invalid_uri_message(uri)
click to toggle source
# File lib/rodauth/features/oauth_dynamic_client_registration.rb, line 221 def register_invalid_uri_message(uri) "The '#{uri}' URL is not allowed by this server." end
register_required_param_message(key)
click to toggle source
# File lib/rodauth/features/oauth_dynamic_client_registration.rb, line 209 def register_required_param_message(key) "The param '#{key}' is required by this server." end
register_throw_json_response_error(code, message)
click to toggle source
# File lib/rodauth/features/oauth_dynamic_client_registration.rb, line 205 def register_throw_json_response_error(code, message) throw_json_response_error(invalid_oauth_response_status, code, message) end
registration_metadata()
click to toggle source
# File lib/rodauth/features/oauth_dynamic_client_registration.rb, line 46 def registration_metadata oauth_server_metadata_body end
require_oauth_application()
click to toggle source
Calls superclass method
# File lib/rodauth/features/oauth_assertion_base.rb, line 26 def require_oauth_application if assertion_grant_type? @oauth_application = __send__(:"require_oauth_application_from_#{assertion_grant_type}_assertion_issuer", param("assertion")) elsif client_assertion_type? @oauth_application = __send__(:"require_oauth_application_from_#{client_assertion_type}_assertion_subject", param("client_assertion")) else return super end redirect_response_error("invalid_grant") unless @oauth_application if client_assertion_type? && (client_id = param_or_nil("client_id")) && client_id != @oauth_application[oauth_applications_client_id_column] # If present, the value of the # "client_id" parameter MUST identify the same client as is # identified by the client assertion. redirect_response_error("invalid_grant") end end
require_oauth_application_from_account()
click to toggle source
# File lib/rodauth/features/oauth_base.rb, line 379 def require_oauth_application_from_account ds = db[oauth_applications_table] .join(oauth_tokens_table, Sequel[oauth_tokens_table][oauth_tokens_oauth_application_id_column] => Sequel[oauth_applications_table][oauth_applications_id_column]) .where(oauth_token_by_token_ds(param("token")).opts.fetch(:where, true)) .where(Sequel[oauth_applications_table][oauth_applications_account_id_column] => account_id) @oauth_application = ds.qualify.first return if @oauth_application set_redirect_error_flash revoke_unauthorized_account_error_flash redirect request.referer || "/" end
require_oauth_application_from_jwt_bearer_assertion_issuer(assertion)
click to toggle source
# File lib/rodauth/features/oauth_jwt_bearer_grant.rb, line 17 def require_oauth_application_from_jwt_bearer_assertion_issuer(assertion) claims = jwt_assertion(assertion) return unless claims db[oauth_applications_table].where( oauth_applications_client_id_column => claims["iss"] ).first end
require_oauth_application_from_jwt_bearer_assertion_subject(assertion)
click to toggle source
# File lib/rodauth/features/oauth_jwt_bearer_grant.rb, line 27 def require_oauth_application_from_jwt_bearer_assertion_subject(assertion) claims = jwt_assertion(assertion) return unless claims db[oauth_applications_table].where( oauth_applications_client_id_column => claims["sub"] ).first end
require_oauth_application_from_saml2_bearer_assertion_issuer(assertion)
click to toggle source
# File lib/rodauth/features/oauth_saml_bearer_grant.rb, line 27 def require_oauth_application_from_saml2_bearer_assertion_issuer(assertion) saml = saml_assertion(assertion) return unless saml db[oauth_applications_table].where( oauth_applications_homepage_url_column => saml.issuers ).first end
require_oauth_application_from_saml2_bearer_assertion_subject(assertion)
click to toggle source
# File lib/rodauth/features/oauth_saml_bearer_grant.rb, line 37 def require_oauth_application_from_saml2_bearer_assertion_subject(assertion) saml = saml_assertion(assertion) return unless saml db[oauth_applications_table].where( oauth_applications_client_id_column => saml.nameid ).first end
rescue_from_uniqueness_error(&block)
click to toggle source
# File lib/rodauth/features/oauth_base.rb, line 301 def rescue_from_uniqueness_error(&block) retries = oauth_unique_id_generation_retries begin transaction(savepoint: :only, &block) rescue Sequel::UniqueConstraintViolation redirect_response_error("already_in_use") if retries.zero? retries -= 1 retry end end
revoke_oauth_token()
click to toggle source
# File lib/rodauth/features/oauth_token_revocation.rb, line 71 def revoke_oauth_token token = param("token") oauth_token = if param("token_type_hint") == "refresh_token" oauth_token_by_refresh_token(token) else oauth_token_by_token(token) end redirect_response_error("invalid_request") unless oauth_token redirect_response_error("invalid_request") unless token_from_application?(oauth_token, oauth_application) update_params = { oauth_tokens_revoked_at_column => Sequel::CURRENT_TIMESTAMP } ds = db[oauth_tokens_table].where(oauth_tokens_id_column => oauth_token[oauth_tokens_id_column]) oauth_token = __update_and_return__(ds, update_params) oauth_token[oauth_tokens_token_column] = token oauth_token # If the particular # token is a refresh token and the authorization server supports the # revocation of access tokens, then the authorization server SHOULD # also invalidate all access tokens based on the same authorization # grant # # we don't need to do anything here, as we revalidate existing tokens end
saml_assertion(assertion)
click to toggle source
# File lib/rodauth/features/oauth_saml_bearer_grant.rb, line 55 def saml_assertion(assertion) settings = OneLogin::RubySaml::Settings.new settings.idp_cert = oauth_saml_cert settings.idp_cert_fingerprint = oauth_saml_cert_fingerprint settings.idp_cert_fingerprint_algorithm = oauth_saml_cert_fingerprint_algorithm settings.name_identifier_format = oauth_saml_name_identifier_format settings.security[:authn_requests_signed] = oauth_saml_security_authn_requests_signed settings.security[:metadata_signed] = oauth_saml_security_metadata_signed settings.security[:digest_method] = oauth_saml_security_digest_method settings.security[:signature_method] = oauth_saml_security_signature_method response = OneLogin::RubySaml::Response.new(assertion, settings: settings, skip_recipient_check: true) # 3. he Assertion MUST have an expiry that limits the time window ... # 4. The Assertion MUST have an expiry that limits the time window ... # 5. The <Subject> element MUST contain at least one ... # 6. The authorization server MUST reject the entire Assertion if the ... # 7. If the Assertion issuer directly authenticated the subject, ... redirect_response_error("invalid_grant") unless response.is_valid? # In order to issue an access token response as described in OAuth 2.0 # [RFC6749] or to rely on an Assertion for client authentication, the # authorization server MUST validate the Assertion according to the # criteria below. # 1. The Assertion's <Issuer> element MUST contain a unique identifier # for the entity that issued the Assertion. redirect_response_error("invalid_grant") unless response.issuers.size == 1 # 2. in addition to the URI references # discussed there, the token endpoint URL of the authorization # server MAY be used as a URI that identifies the authorization # server as an intended audience. The authorization server MUST # reject any Assertion that does not contain its own identity as # the intended audience. redirect_response_error("invalid_grant") if response.audiences && !response.audiences.include?(token_url) response end
scopes()
click to toggle source
# File lib/rodauth/features/oauth_base.rb, line 183 def scopes scope = request.params["scope"] case scope when Array scope when String scope.split(" ") when nil Array(oauth_application_default_scope) end end
secret_hash(secret)
click to toggle source
# File lib/rodauth/features/oauth_base.rb, line 397 def secret_hash(secret) password_hash(secret) end
secret_matches?(oauth_application, secret)
click to toggle source
# File lib/rodauth/features/oauth_base.rb, line 393 def secret_matches?(oauth_application, secret) BCrypt::Password.new(oauth_application[oauth_applications_client_secret_column]) == secret end
session_value()
click to toggle source
Overrides session_value
, so that a valid authorization token also authenticates a request
Calls superclass method
# File lib/rodauth/features/oauth_base.rb, line 160 def session_value super || begin return unless authorization_token authorization_token[oauth_tokens_account_id_column] end end
supported_grant_type?(grant_type, expected_grant_type = grant_type)
click to toggle source
# File lib/rodauth/features/oauth_base.rb, line 619 def supported_grant_type?(grant_type, expected_grant_type = grant_type) return false unless grant_type == expected_grant_type return true unless (grant_types_supported = oauth_application[oauth_applications_grant_types_column]) grant_types_supported = grant_types_supported.split(/ +/) grant_types_supported.include?(grant_type) end
template_path(page)
click to toggle source
Calls superclass method
# File lib/rodauth/features/oauth_base.rb, line 325 def template_path(page) path = File.join(File.dirname(__FILE__), "../../../templates", "#{page}.str") return super unless File.exist?(path) path end
throw_json_response_error(status, error_code, message = nil)
click to toggle source
# File lib/rodauth/features/oauth_base.rb, line 694 def throw_json_response_error(status, error_code, message = nil) set_response_error_status(status) code = if respond_to?(:"#{error_code}_error_code") send(:"#{error_code}_error_code") else error_code end payload = { "error" => code } payload["error_description"] = message || (send(:"#{error_code}_message") if respond_to?(:"#{error_code}_message")) json_payload = _json_response_body(payload) response["Content-Type"] ||= json_response_content_type response["WWW-Authenticate"] = oauth_token_type.upcase if status == 401 response.write(json_payload) request.halt end
token_from_application?(oauth_token, oauth_application)
click to toggle source
# File lib/rodauth/features/oauth_base.rb, line 409 def token_from_application?(oauth_token, oauth_application) oauth_token[oauth_tokens_oauth_application_id_column] == oauth_application[oauth_applications_id_column] end
try_approval_prompt()
click to toggle source
# File lib/rodauth/features/oauth_authorization_code_grant.rb, line 87 def try_approval_prompt approval_prompt = param_or_nil("approval_prompt") return unless approval_prompt && approval_prompt == "auto" return 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? # if there's a previous oauth grant for the params combo, it means that this user has approved before. request.env["REQUEST_METHOD"] = "POST" end
try_prompt()
click to toggle source
this executes before checking for a logged in account
# File lib/rodauth/features/oidc.rb, line 260 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" # only works if select_account plugin is available require_select_account if respond_to?(:require_select_account) else redirect_response_error("invalid_request") end end
use_date_arithmetic?()
click to toggle source
# File lib/rodauth/features/oauth_base.rb, line 271 def use_date_arithmetic? true end
validate_client_registration_params()
click to toggle source
# File lib/rodauth/features/oauth_dynamic_client_registration.rb, line 50 def validate_client_registration_params oauth_client_registration_required_params.each do |required_param| unless request.params.key?(required_param) register_throw_json_response_error("invalid_client_metadata", register_required_param_message(required_param)) end end metadata = registration_metadata @oauth_application_params = request.params.each_with_object({}) do |(key, value), params| case key when "redirect_uris" if value.is_a?(Array) value = value.each do |uri| register_throw_json_response_error("invalid_redirect_uri", register_invalid_uri_message(uri)) unless check_valid_uri?(uri) end.join(" ") else register_throw_json_response_error("invalid_redirect_uri", register_invalid_uri_message(value)) end key = oauth_applications_redirect_uri_column when "token_endpoint_auth_method" unless oauth_auth_methods_supported.include?(value) register_throw_json_response_error("invalid_client_metadata", register_invalid_param_message(key)) end # verify if in range key = oauth_applications_token_endpoint_auth_method_column when "grant_types" if value.is_a?(Array) value = value.each do |grant_type| unless metadata[:grant_types_supported].include?(grant_type) register_throw_json_response_error("invalid_client_metadata", register_invalid_grant_type_message(grant_type)) end end.join(" ") else set_field_error(key, invalid_client_metadata_message) end key = oauth_applications_grant_types_column when "response_types" if value.is_a?(Array) grant_types = request.params["grant_types"] || metadata[:grant_types_supported] value = value.each do |response_type| unless metadata[:response_types_supported].include?(response_type) register_throw_json_response_error("invalid_client_metadata", register_invalid_response_type_message(response_type)) end validate_client_registration_response_type(response_type, grant_types) end.join(" ") else set_field_error(key, invalid_client_metadata_message) end key = oauth_applications_response_types_column # verify if in range and match grant type when "client_uri", "logo_uri", "tos_uri", "policy_uri", "jwks_uri" register_throw_json_response_error("invalid_client_metadata", register_invalid_uri_message(value)) unless check_valid_uri?(value) case key when "client_uri" key = "homepage_url" when "jwks_uri" if request.params.key?("jwks") register_throw_json_response_error("invalid_client_metadata", register_invalid_jwks_param_message(key, "jwks")) end end key = __send__(:"oauth_applications_#{key}_column") when "jwks" register_throw_json_response_error("invalid_client_metadata", register_invalid_param_message(value)) unless value.is_a?(Hash) if request.params.key?("jwks_uri") register_throw_json_response_error("invalid_client_metadata", register_invalid_jwks_param_message(key, "jwks_uri")) end key = oauth_applications_jwks_column value = JSON.dump(value) when "scope" scopes = value.split(" ") - oauth_application_scopes register_throw_json_response_error("invalid_client_metadata", register_invalid_scopes_message(value)) unless scopes.empty? key = oauth_applications_scopes_column # verify if in range when "contacts" register_throw_json_response_error("invalid_client_metadata", register_invalid_contacts_message(value)) unless value.is_a?(Array) value = value.join(" ") key = oauth_applications_contacts_column when "client_name" key = oauth_applications_name_column else if respond_to?(:"oauth_applications_#{key}_column") property = :"oauth_applications_#{key}_column" if PROTECTED_APPLICATION_ATTRIBUTES.include?(property) register_throw_json_response_error("invalid_client_metadata", register_invalid_param_message(key)) end key = __send__(property) elsif !db[oauth_applications_table].columns.include?(key.to_sym) register_throw_json_response_error("invalid_client_metadata", register_invalid_param_message(key)) end end params[key] = value end end
validate_client_registration_response_type(response_type, grant_types)
click to toggle source
# File lib/rodauth/features/oauth_dynamic_client_registration.rb, line 149 def validate_client_registration_response_type(response_type, grant_types) case response_type when "code" unless grant_types.include?("authorization_code") register_throw_json_response_error("invalid_client_metadata", register_invalid_response_type_for_grant_type_message(response_type, "authorization_code")) end when "token" unless grant_types.include?("implicit") register_throw_json_response_error("invalid_client_metadata", register_invalid_response_type_for_grant_type_message(response_type, "implicit")) end when "none" if grant_types.include?("implicit") || grant_types.include?("authorization_code") register_throw_json_response_error("invalid_client_metadata", register_invalid_response_type_message(response_type)) end end end
validate_oauth_application_params()
click to toggle source
# File lib/rodauth/features/oauth_application_management.rb, line 161 def validate_oauth_application_params oauth_application_params.each do |key, value| if key == oauth_application_homepage_url_param set_field_error(key, invalid_url_message) unless check_valid_uri?(value) elsif key == oauth_application_redirect_uri_param if value.respond_to?(:each) value.each do |uri| next if uri.empty? set_field_error(key, invalid_url_message) unless check_valid_uri?(uri) end else set_field_error(key, invalid_url_message) unless check_valid_uri?(value) end elsif key == oauth_application_scopes_param value.each do |scope| set_field_error(key, invalid_scope_message) unless oauth_application_scopes.include?(scope) end end end throw :rodauth_error if @field_errors && !@field_errors.empty? end
validate_oauth_grant_params()
click to toggle source
# File lib/rodauth/features/oauth_authorization_code_grant.rb, line 68 def validate_oauth_grant_params redirect_response_error("invalid_request", request.referer || default_redirect) unless oauth_application && check_valid_redirect_uri? unless oauth_application && check_valid_redirect_uri? && check_valid_access_type? && check_valid_approval_prompt? && check_valid_response_type? redirect_response_error("invalid_request") end redirect_response_error("invalid_scope") unless check_valid_scopes? return unless (response_mode = param_or_nil("response_mode")) && response_mode != "form_post" redirect_response_error("invalid_request") end
validate_oauth_introspect_params(token_hint_types = %w[access_token refresh_token].freeze)
click to toggle source
Token introspect
# File lib/rodauth/features/oauth_token_introspection.rb, line 49 def validate_oauth_introspect_params(token_hint_types = %w[access_token refresh_token].freeze) # check if valid token hint type if param_or_nil("token_type_hint") && !token_hint_types.include?(param("token_type_hint")) redirect_response_error("unsupported_token_type") end redirect_response_error("invalid_request") unless param_or_nil("token") end
validate_oauth_revoke_params()
click to toggle source
Calls superclass method
# File lib/rodauth/features/oauth_jwt.rb, line 698 def validate_oauth_revoke_params token_hint = param_or_nil("token_type_hint") throw(:rodauth_error) if !token_hint || token_hint == "access_token" super end
validate_oauth_token_params()
click to toggle source
Calls superclass method
# File lib/rodauth/features/oauth_assertion_base.rb, line 20 def validate_oauth_token_params return super unless assertion_grant_type? redirect_response_error("invalid_grant") unless param_or_nil("assertion") end
validate_oidc_logout_params()
click to toggle source
Logout
# File lib/rodauth/features/oidc.rb, line 463 def validate_oidc_logout_params redirect_response_error("invalid_request") unless param_or_nil("id_token_hint") # check if valid token hint type return unless (redirect_uri = param_or_nil("post_logout_redirect_uri")) return if check_valid_uri?(redirect_uri) redirect_response_error("invalid_request") end
validate_pkce_challenge_params()
click to toggle source
# File lib/rodauth/features/oauth_pkce.rb, line 64 def validate_pkce_challenge_params if param_or_nil("code_challenge") challenge_method = param_or_nil("code_challenge_method") redirect_response_error("code_challenge_required") unless oauth_pkce_challenge_method == challenge_method else return unless oauth_require_pkce redirect_response_error("code_challenge_required") end end
verify_aud(expected_aud, aud)
click to toggle source
# File lib/rodauth/features/oauth_jwt.rb, line 355 def verify_aud(expected_aud, aud) expected_aud == aud end
verify_jti(jti, claims)
click to toggle source
# File lib/rodauth/features/oauth_jwt.rb, line 351 def verify_jti(jti, claims) generate_jti(claims) == jti end
webfinger()
click to toggle source
# File lib/rodauth/features/oidc.rb, line 220 def webfinger request.on(".well-known/webfinger") do request.get do resource = param_or_nil("resource") throw_json_response_error(400, "invalid_request") unless resource response.status = 200 response["Content-Type"] ||= "application/jrd+json" json_payload = JSON.dump({ subject: resource, links: [{ rel: webfinger_relation, href: authorization_server_url }] }) response.write(json_payload) request.halt end end end