rodauth-oauth 0.0.1 → 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +148 -1
- data/LICENSE.txt +191 -0
- data/README.md +309 -21
- data/lib/generators/roda/oauth/install_generator.rb +1 -1
- data/lib/generators/roda/oauth/templates/db/migrate/create_rodauth_oauth.rb +12 -0
- data/lib/generators/roda/oauth/views_generator.rb +1 -6
- data/lib/rodauth/features/oauth.rb +740 -301
- data/lib/rodauth/features/oauth_http_mac.rb +108 -0
- data/lib/rodauth/features/oauth_jwt.rb +421 -0
- data/lib/rodauth/oauth/ttl_store.rb +59 -0
- data/lib/rodauth/oauth/version.rb +1 -1
- metadata +7 -2
| @@ -0,0 +1,108 @@ | |
| 1 | 
            +
            # frozen-string-literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Rodauth
         | 
| 4 | 
            +
              Feature.define(:oauth_http_mac) do
         | 
| 5 | 
            +
                unless String.method_defined?(:delete_prefix)
         | 
| 6 | 
            +
                  module PrefixExtensions
         | 
| 7 | 
            +
                    refine(String) do
         | 
| 8 | 
            +
                      def delete_suffix(suffix)
         | 
| 9 | 
            +
                        suffix = suffix.to_s
         | 
| 10 | 
            +
                        len = suffix.length
         | 
| 11 | 
            +
                        if len.positive? && index(suffix, -len)
         | 
| 12 | 
            +
                          self[0...-len]
         | 
| 13 | 
            +
                        else
         | 
| 14 | 
            +
                          dup
         | 
| 15 | 
            +
                        end
         | 
| 16 | 
            +
                      end
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                      def delete_prefix(prefix)
         | 
| 19 | 
            +
                        prefix = prefix.to_s
         | 
| 20 | 
            +
                        if rindex(prefix, 0)
         | 
| 21 | 
            +
                          self[prefix.length..-1]
         | 
| 22 | 
            +
                        else
         | 
| 23 | 
            +
                          dup
         | 
| 24 | 
            +
                        end
         | 
| 25 | 
            +
                      end
         | 
| 26 | 
            +
                    end
         | 
| 27 | 
            +
                  end
         | 
| 28 | 
            +
                  using(PrefixExtensions)
         | 
| 29 | 
            +
                end
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                depends :oauth
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                auth_value_method :oauth_token_type, "mac"
         | 
| 34 | 
            +
                auth_value_method :oauth_mac_algorithm, "hmac-sha-256" # hmac-sha-256, hmac-sha-1
         | 
| 35 | 
            +
                auth_value_method :oauth_tokens_mac_key_column, :mac_key
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                def authorization_token
         | 
| 38 | 
            +
                  return @authorization_token if defined?(@authorization_token)
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                  @authorization_token = begin
         | 
| 41 | 
            +
                    value = request.get_header("HTTP_AUTHORIZATION").to_s
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                    scheme, token = value.split(/ +/, 2)
         | 
| 44 | 
            +
             | 
| 45 | 
            +
                    return unless scheme == "MAC"
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                    mac_attributes = parse_mac_authorization_header_props(token)
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                    oauth_token = oauth_token_by_token(mac_attributes["id"])
         | 
| 50 | 
            +
             | 
| 51 | 
            +
                    return unless oauth_token && mac_signature_matches?(oauth_token, mac_attributes)
         | 
| 52 | 
            +
             | 
| 53 | 
            +
                    oauth_token
         | 
| 54 | 
            +
             | 
| 55 | 
            +
                    # TODO: set new MAC-KEY for the next request
         | 
| 56 | 
            +
                  end
         | 
| 57 | 
            +
                end
         | 
| 58 | 
            +
             | 
| 59 | 
            +
                private
         | 
| 60 | 
            +
             | 
| 61 | 
            +
                def generate_oauth_token(params = {}, *args)
         | 
| 62 | 
            +
                  super({ oauth_tokens_mac_key_column => oauth_unique_id_generator }.merge(params), *args)
         | 
| 63 | 
            +
                end
         | 
| 64 | 
            +
             | 
| 65 | 
            +
                def json_access_token_payload(oauth_token)
         | 
| 66 | 
            +
                  payload = super
         | 
| 67 | 
            +
             | 
| 68 | 
            +
                  payload["mac_key"] = oauth_token[oauth_tokens_mac_key_column]
         | 
| 69 | 
            +
                  payload["mac_algorithm"] = oauth_mac_algorithm
         | 
| 70 | 
            +
             | 
| 71 | 
            +
                  payload
         | 
| 72 | 
            +
                end
         | 
| 73 | 
            +
             | 
| 74 | 
            +
                def mac_signature_matches?(oauth_token, mac_attributes)
         | 
| 75 | 
            +
                  nonce = mac_attributes["nonce"]
         | 
| 76 | 
            +
                  uri = URI(request.url)
         | 
| 77 | 
            +
             | 
| 78 | 
            +
                  request_signature = [
         | 
| 79 | 
            +
                    nonce,
         | 
| 80 | 
            +
                    request.request_method,
         | 
| 81 | 
            +
                    uri.request_uri,
         | 
| 82 | 
            +
                    uri.host,
         | 
| 83 | 
            +
                    uri.port
         | 
| 84 | 
            +
                  ].join("\n") + ("\n" * 3)
         | 
| 85 | 
            +
             | 
| 86 | 
            +
                  mac_algorithm = case oauth_mac_algorithm
         | 
| 87 | 
            +
                                  when "hmac-sha-256"
         | 
| 88 | 
            +
                                    OpenSSL::Digest::SHA256
         | 
| 89 | 
            +
                                  when "hmac-sha-1"
         | 
| 90 | 
            +
                                    OpenSSL::Digest::SHA1
         | 
| 91 | 
            +
                                  else
         | 
| 92 | 
            +
                                    raise ArgumentError, "Unsupported algorithm"
         | 
| 93 | 
            +
                                  end
         | 
| 94 | 
            +
             | 
| 95 | 
            +
                  mac_signature = Base64.strict_encode64 \
         | 
| 96 | 
            +
                    OpenSSL::HMAC.digest(mac_algorithm.new, oauth_token[oauth_tokens_mac_key_column], request_signature)
         | 
| 97 | 
            +
             | 
| 98 | 
            +
                  mac_signature == mac_attributes["mac"]
         | 
| 99 | 
            +
                end
         | 
| 100 | 
            +
             | 
| 101 | 
            +
                def parse_mac_authorization_header_props(token)
         | 
| 102 | 
            +
                  @mac_authorization_header_props = token.split(/ *, */).each_with_object({}) do |prop, props|
         | 
| 103 | 
            +
                    field, value = prop.split(/ *= */, 2)
         | 
| 104 | 
            +
                    props[field] = value.delete_prefix("\"").delete_suffix("\"")
         | 
| 105 | 
            +
                  end
         | 
| 106 | 
            +
                end
         | 
| 107 | 
            +
              end
         | 
| 108 | 
            +
            end
         | 
| @@ -0,0 +1,421 @@ | |
| 1 | 
            +
            # frozen-string-literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require "rodauth/oauth/ttl_store"
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            module Rodauth
         | 
| 6 | 
            +
              Feature.define(:oauth_jwt) do
         | 
| 7 | 
            +
                depends :oauth
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                auth_value_method :oauth_jwt_token_issuer, "Example"
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                auth_value_method :oauth_application_jws_jwk_column, nil
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                auth_value_method :oauth_jwt_key, nil
         | 
| 14 | 
            +
                auth_value_method :oauth_jwt_public_key, nil
         | 
| 15 | 
            +
                auth_value_method :oauth_jwt_algorithm, "HS256"
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                auth_value_method :oauth_jwt_jwe_key, nil
         | 
| 18 | 
            +
                auth_value_method :oauth_jwt_jwe_public_key, nil
         | 
| 19 | 
            +
                auth_value_method :oauth_jwt_jwe_algorithm, nil
         | 
| 20 | 
            +
                auth_value_method :oauth_jwt_jwe_encryption_method, nil
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                auth_value_method :oauth_jwt_jwe_copyright, nil
         | 
| 23 | 
            +
                auth_value_method :oauth_jwt_audience, nil
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                auth_value_method :request_uri_not_supported_message, "request uri is unsupported"
         | 
| 26 | 
            +
                auth_value_method :invalid_request_object_message, "request object is invalid"
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                auth_value_methods(
         | 
| 29 | 
            +
                  :jwt_encode,
         | 
| 30 | 
            +
                  :jwt_decode,
         | 
| 31 | 
            +
                  :jwks_set
         | 
| 32 | 
            +
                )
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                JWKS = OAuth::TtlStore.new
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                def require_oauth_authorization(*scopes)
         | 
| 37 | 
            +
                  authorization_required unless authorization_token
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                  scopes << oauth_application_default_scope if scopes.empty?
         | 
| 40 | 
            +
             | 
| 41 | 
            +
                  token_scopes = authorization_token["scope"].split(" ")
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                  authorization_required unless scopes.any? { |scope| token_scopes.include?(scope) }
         | 
| 44 | 
            +
                end
         | 
| 45 | 
            +
             | 
| 46 | 
            +
                private
         | 
| 47 | 
            +
             | 
| 48 | 
            +
                def authorization_token
         | 
| 49 | 
            +
                  return @authorization_token if defined?(@authorization_token)
         | 
| 50 | 
            +
             | 
| 51 | 
            +
                  @authorization_token = begin
         | 
| 52 | 
            +
                    bearer_token = fetch_access_token
         | 
| 53 | 
            +
             | 
| 54 | 
            +
                    return unless bearer_token
         | 
| 55 | 
            +
             | 
| 56 | 
            +
                    jwt_token = jwt_decode(bearer_token)
         | 
| 57 | 
            +
             | 
| 58 | 
            +
                    return unless jwt_token
         | 
| 59 | 
            +
             | 
| 60 | 
            +
                    return if jwt_token["iss"] != oauth_jwt_token_issuer ||
         | 
| 61 | 
            +
                              jwt_token["aud"] != oauth_jwt_audience ||
         | 
| 62 | 
            +
                              !jwt_token["sub"]
         | 
| 63 | 
            +
             | 
| 64 | 
            +
                    jwt_token
         | 
| 65 | 
            +
                  end
         | 
| 66 | 
            +
                end
         | 
| 67 | 
            +
             | 
| 68 | 
            +
                # /authorize
         | 
| 69 | 
            +
             | 
| 70 | 
            +
                def validate_oauth_grant_params
         | 
| 71 | 
            +
                  # TODO: add support for requst_uri
         | 
| 72 | 
            +
                  redirect_response_error("request_uri_not_supported") if param_or_nil("request_uri")
         | 
| 73 | 
            +
             | 
| 74 | 
            +
                  request_object = param_or_nil("request")
         | 
| 75 | 
            +
             | 
| 76 | 
            +
                  return super unless request_object && oauth_application
         | 
| 77 | 
            +
             | 
| 78 | 
            +
                  jws_jwk = if oauth_application[oauth_application_jws_jwk_column]
         | 
| 79 | 
            +
                              jwk = oauth_application[oauth_application_jws_jwk_column]
         | 
| 80 | 
            +
             | 
| 81 | 
            +
                              if jwk
         | 
| 82 | 
            +
                                jwk = JSON.parse(jwk, symbolize_names: true) if jwk.is_a?(String)
         | 
| 83 | 
            +
                              end
         | 
| 84 | 
            +
                            else
         | 
| 85 | 
            +
                              redirect_response_error("invalid_request_object")
         | 
| 86 | 
            +
                            end
         | 
| 87 | 
            +
             | 
| 88 | 
            +
                  claims = jwt_decode(request_object, jws_key: jwk_import(jws_jwk), jws_algorithm: jwk[:alg])
         | 
| 89 | 
            +
             | 
| 90 | 
            +
                  redirect_response_error("invalid_request_object") unless claims
         | 
| 91 | 
            +
             | 
| 92 | 
            +
                  # If signed, the Authorization Request
         | 
| 93 | 
            +
                  # Object SHOULD contain the Claims "iss" (issuer) and "aud" (audience)
         | 
| 94 | 
            +
                  # as members, with their semantics being the same as defined in the JWT
         | 
| 95 | 
            +
                  # [RFC7519] specification.  The value of "aud" should be the value of
         | 
| 96 | 
            +
                  # the Authorization Server (AS) "issuer" as defined in RFC8414
         | 
| 97 | 
            +
                  # [RFC8414].
         | 
| 98 | 
            +
                  claims.delete(:iss)
         | 
| 99 | 
            +
                  audience = claims.delete(:aud)
         | 
| 100 | 
            +
             | 
| 101 | 
            +
                  redirect_response_error("invalid_request_object") if audience && audience != authorization_server_url
         | 
| 102 | 
            +
             | 
| 103 | 
            +
                  claims.each do |k, v|
         | 
| 104 | 
            +
                    request.params[k.to_s] = v
         | 
| 105 | 
            +
                  end
         | 
| 106 | 
            +
             | 
| 107 | 
            +
                  super
         | 
| 108 | 
            +
                end
         | 
| 109 | 
            +
             | 
| 110 | 
            +
                # /token
         | 
| 111 | 
            +
             | 
| 112 | 
            +
                def before_token
         | 
| 113 | 
            +
                  # requset authentication optional for assertions
         | 
| 114 | 
            +
                  return if param("grant_type") == "urn:ietf:params:oauth:grant-type:jwt-bearer"
         | 
| 115 | 
            +
             | 
| 116 | 
            +
                  super
         | 
| 117 | 
            +
                end
         | 
| 118 | 
            +
             | 
| 119 | 
            +
                def validate_oauth_token_params
         | 
| 120 | 
            +
                  if param("grant_type") == "urn:ietf:params:oauth:grant-type:jwt-bearer"
         | 
| 121 | 
            +
                    redirect_response_error("invalid_client") unless param_or_nil("assertion")
         | 
| 122 | 
            +
                  else
         | 
| 123 | 
            +
                    super
         | 
| 124 | 
            +
                  end
         | 
| 125 | 
            +
                end
         | 
| 126 | 
            +
             | 
| 127 | 
            +
                def create_oauth_token
         | 
| 128 | 
            +
                  if param("grant_type") == "urn:ietf:params:oauth:grant-type:jwt-bearer"
         | 
| 129 | 
            +
                    create_oauth_token_from_assertion
         | 
| 130 | 
            +
                  else
         | 
| 131 | 
            +
                    super
         | 
| 132 | 
            +
                  end
         | 
| 133 | 
            +
                end
         | 
| 134 | 
            +
             | 
| 135 | 
            +
                def create_oauth_token_from_assertion
         | 
| 136 | 
            +
                  claims = jwt_decode(param("assertion"))
         | 
| 137 | 
            +
             | 
| 138 | 
            +
                  redirect_response_error("invalid_grant") unless claims
         | 
| 139 | 
            +
             | 
| 140 | 
            +
                  @oauth_application = db[oauth_applications_table].where(oauth_applications_client_id_column => claims["client_id"]).first
         | 
| 141 | 
            +
             | 
| 142 | 
            +
                  account = account_ds(claims["sub"]).first
         | 
| 143 | 
            +
             | 
| 144 | 
            +
                  redirect_response_error("invalid_client") unless oauth_application && account
         | 
| 145 | 
            +
             | 
| 146 | 
            +
                  create_params = {
         | 
| 147 | 
            +
                    oauth_tokens_account_id_column => claims["sub"],
         | 
| 148 | 
            +
                    oauth_tokens_oauth_application_id_column => oauth_application[oauth_applications_id_column],
         | 
| 149 | 
            +
                    oauth_tokens_scopes_column => claims["scope"]
         | 
| 150 | 
            +
                  }
         | 
| 151 | 
            +
             | 
| 152 | 
            +
                  generate_oauth_token(create_params, false)
         | 
| 153 | 
            +
                end
         | 
| 154 | 
            +
             | 
| 155 | 
            +
                def generate_oauth_token(params = {}, should_generate_refresh_token = true)
         | 
| 156 | 
            +
                  create_params = {
         | 
| 157 | 
            +
                    oauth_grants_expires_in_column => Time.now + oauth_token_expires_in
         | 
| 158 | 
            +
                  }.merge(params)
         | 
| 159 | 
            +
             | 
| 160 | 
            +
                  if should_generate_refresh_token
         | 
| 161 | 
            +
                    refresh_token = oauth_unique_id_generator
         | 
| 162 | 
            +
             | 
| 163 | 
            +
                    if oauth_tokens_refresh_token_hash_column
         | 
| 164 | 
            +
                      create_params[oauth_tokens_refresh_token_hash_column] = generate_token_hash(refresh_token)
         | 
| 165 | 
            +
                    else
         | 
| 166 | 
            +
                      create_params[oauth_tokens_refresh_token_column] = refresh_token
         | 
| 167 | 
            +
                    end
         | 
| 168 | 
            +
                  end
         | 
| 169 | 
            +
             | 
| 170 | 
            +
                  oauth_token = _generate_oauth_token(create_params)
         | 
| 171 | 
            +
             | 
| 172 | 
            +
                  issued_at = Time.now.utc.to_i
         | 
| 173 | 
            +
             | 
| 174 | 
            +
                  payload = {
         | 
| 175 | 
            +
                    sub: oauth_token[oauth_tokens_account_id_column],
         | 
| 176 | 
            +
                    iss: oauth_jwt_token_issuer, # issuer
         | 
| 177 | 
            +
                    iat: issued_at, # issued at
         | 
| 178 | 
            +
                    #
         | 
| 179 | 
            +
                    # sub  REQUIRED - as defined in section 4.1.2 of [RFC7519].  In case of
         | 
| 180 | 
            +
                    # access tokens obtained through grants where a resource owner is
         | 
| 181 | 
            +
                    # involved, such as the authorization code grant, the value of "sub"
         | 
| 182 | 
            +
                    # SHOULD correspond to the subject identifier of the resource owner.
         | 
| 183 | 
            +
                    # In case of access tokens obtained through grants where no resource
         | 
| 184 | 
            +
                    # owner is involved, such as the client credentials grant, the value
         | 
| 185 | 
            +
                    # of "sub" SHOULD correspond to an identifier the authorization
         | 
| 186 | 
            +
                    # server uses to indicate the client application.
         | 
| 187 | 
            +
                    client_id: oauth_application[oauth_applications_client_id_column],
         | 
| 188 | 
            +
             | 
| 189 | 
            +
                    exp: issued_at + oauth_token_expires_in,
         | 
| 190 | 
            +
                    aud: oauth_jwt_audience,
         | 
| 191 | 
            +
             | 
| 192 | 
            +
                    # one of the points of using jwt is avoiding database lookups, so we put here all relevant
         | 
| 193 | 
            +
                    # token data.
         | 
| 194 | 
            +
                    scope: oauth_token[oauth_tokens_scopes_column]
         | 
| 195 | 
            +
                  }
         | 
| 196 | 
            +
             | 
| 197 | 
            +
                  token = jwt_encode(payload)
         | 
| 198 | 
            +
             | 
| 199 | 
            +
                  oauth_token[oauth_tokens_token_column] = token
         | 
| 200 | 
            +
                  oauth_token
         | 
| 201 | 
            +
                end
         | 
| 202 | 
            +
             | 
| 203 | 
            +
                def oauth_token_by_token(token, *)
         | 
| 204 | 
            +
                  jwt_decode(token)
         | 
| 205 | 
            +
                end
         | 
| 206 | 
            +
             | 
| 207 | 
            +
                def json_token_introspect_payload(oauth_token)
         | 
| 208 | 
            +
                  return { active: false } unless oauth_token
         | 
| 209 | 
            +
             | 
| 210 | 
            +
                  return super unless oauth_token["sub"] # naive check on whether it's a jwt token
         | 
| 211 | 
            +
             | 
| 212 | 
            +
                  {
         | 
| 213 | 
            +
                    active: true,
         | 
| 214 | 
            +
                    scope: oauth_token["scope"],
         | 
| 215 | 
            +
                    client_id: oauth_token["client_id"],
         | 
| 216 | 
            +
                    # username
         | 
| 217 | 
            +
                    token_type: "access_token",
         | 
| 218 | 
            +
                    exp: oauth_token["exp"],
         | 
| 219 | 
            +
                    iat: oauth_token["iat"],
         | 
| 220 | 
            +
                    nbf: oauth_token["nbf"],
         | 
| 221 | 
            +
                    sub: oauth_token["sub"],
         | 
| 222 | 
            +
                    aud: oauth_token["aud"],
         | 
| 223 | 
            +
                    iss: oauth_token["iss"],
         | 
| 224 | 
            +
                    jti: oauth_token["jti"]
         | 
| 225 | 
            +
                  }
         | 
| 226 | 
            +
                end
         | 
| 227 | 
            +
             | 
| 228 | 
            +
                def oauth_server_metadata_body(path)
         | 
| 229 | 
            +
                  metadata = super
         | 
| 230 | 
            +
                  metadata.merge! \
         | 
| 231 | 
            +
                    jwks_uri: oauth_jwks_url,
         | 
| 232 | 
            +
                    token_endpoint_auth_signing_alg_values_supported: [oauth_jwt_algorithm]
         | 
| 233 | 
            +
                  metadata
         | 
| 234 | 
            +
                end
         | 
| 235 | 
            +
             | 
| 236 | 
            +
                def token_from_application?(oauth_token, oauth_application)
         | 
| 237 | 
            +
                  return super unless oauth_token["sub"] # naive check on whether it's a jwt token
         | 
| 238 | 
            +
             | 
| 239 | 
            +
                  oauth_token["client_id"] == oauth_application[oauth_applications_client_id_column]
         | 
| 240 | 
            +
                end
         | 
| 241 | 
            +
             | 
| 242 | 
            +
                def _jwt_key
         | 
| 243 | 
            +
                  @_jwt_key ||= oauth_jwt_key || (oauth_application[oauth_applications_client_secret_column] if oauth_application)
         | 
| 244 | 
            +
                end
         | 
| 245 | 
            +
             | 
| 246 | 
            +
                # Resource Server only!
         | 
| 247 | 
            +
                #
         | 
| 248 | 
            +
                # returns the jwks set from the authorization server.
         | 
| 249 | 
            +
                def auth_server_jwks_set
         | 
| 250 | 
            +
                  metadata = authorization_server_metadata
         | 
| 251 | 
            +
             | 
| 252 | 
            +
                  return unless metadata && (jwks_uri = metadata[:jwks_uri])
         | 
| 253 | 
            +
             | 
| 254 | 
            +
                  jwks_uri = URI(jwks_uri)
         | 
| 255 | 
            +
             | 
| 256 | 
            +
                  jwks = JWKS[jwks_uri]
         | 
| 257 | 
            +
             | 
| 258 | 
            +
                  return jwks if jwks
         | 
| 259 | 
            +
             | 
| 260 | 
            +
                  JWKS.set(jwks_uri) do
         | 
| 261 | 
            +
                    http = Net::HTTP.new(jwks_uri.host, jwks_uri.port)
         | 
| 262 | 
            +
                    http.use_ssl = jwks_uri.scheme == "https"
         | 
| 263 | 
            +
             | 
| 264 | 
            +
                    request = Net::HTTP::Get.new(jwks_uri.request_uri)
         | 
| 265 | 
            +
                    request["accept"] = json_response_content_type
         | 
| 266 | 
            +
                    response = http.request(request)
         | 
| 267 | 
            +
                    authorization_required unless response.code.to_i == 200
         | 
| 268 | 
            +
             | 
| 269 | 
            +
                    # time-to-live
         | 
| 270 | 
            +
                    ttl = if response.key?("cache-control")
         | 
| 271 | 
            +
                            cache_control = response["cache_control"]
         | 
| 272 | 
            +
                            cache_control[/max-age=(\d+)/, 1]
         | 
| 273 | 
            +
                          elsif response.key?("expires")
         | 
| 274 | 
            +
                            Time.httpdate(response["expires"]).utc.to_i - Time.now.utc.to_i
         | 
| 275 | 
            +
                          end
         | 
| 276 | 
            +
             | 
| 277 | 
            +
                    [JSON.parse(response.body, symbolize_names: true), ttl]
         | 
| 278 | 
            +
                  end
         | 
| 279 | 
            +
                end
         | 
| 280 | 
            +
             | 
| 281 | 
            +
                if defined?(JSON::JWT)
         | 
| 282 | 
            +
                  # :nocov:
         | 
| 283 | 
            +
             | 
| 284 | 
            +
                  def jwk_import(data)
         | 
| 285 | 
            +
                    JSON::JWK.new(data)
         | 
| 286 | 
            +
                  end
         | 
| 287 | 
            +
             | 
| 288 | 
            +
                  # json-jwt
         | 
| 289 | 
            +
                  def jwt_encode(payload)
         | 
| 290 | 
            +
                    jwt = JSON::JWT.new(payload)
         | 
| 291 | 
            +
                    jwk = JSON::JWK.new(_jwt_key)
         | 
| 292 | 
            +
             | 
| 293 | 
            +
                    jwt = jwt.sign(jwk, oauth_jwt_algorithm)
         | 
| 294 | 
            +
                    jwt.kid = jwk.thumbprint
         | 
| 295 | 
            +
             | 
| 296 | 
            +
                    if oauth_jwt_jwe_key
         | 
| 297 | 
            +
                      algorithm = oauth_jwt_jwe_algorithm.to_sym if oauth_jwt_jwe_algorithm
         | 
| 298 | 
            +
                      jwt = jwt.encrypt(oauth_jwt_jwe_public_key || oauth_jwt_jwe_key,
         | 
| 299 | 
            +
                                        algorithm,
         | 
| 300 | 
            +
                                        oauth_jwt_jwe_encryption_method.to_sym)
         | 
| 301 | 
            +
                    end
         | 
| 302 | 
            +
                    jwt.to_s
         | 
| 303 | 
            +
                  end
         | 
| 304 | 
            +
             | 
| 305 | 
            +
                  def jwt_decode(token, jws_key: oauth_jwt_public_key || _jwt_key, **)
         | 
| 306 | 
            +
                    token = JSON::JWT.decode(token, oauth_jwt_jwe_key).plain_text if oauth_jwt_jwe_key
         | 
| 307 | 
            +
             | 
| 308 | 
            +
                    @jwt_token = if jws_key
         | 
| 309 | 
            +
                                   JSON::JWT.decode(token, jws_key)
         | 
| 310 | 
            +
                                 elsif !is_authorization_server? && auth_server_jwks_set
         | 
| 311 | 
            +
                                   JSON::JWT.decode(token, JSON::JWK::Set.new(auth_server_jwks_set))
         | 
| 312 | 
            +
                                 end
         | 
| 313 | 
            +
                  rescue JSON::JWT::Exception
         | 
| 314 | 
            +
                    nil
         | 
| 315 | 
            +
                  end
         | 
| 316 | 
            +
             | 
| 317 | 
            +
                  def jwks_set
         | 
| 318 | 
            +
                    [
         | 
| 319 | 
            +
                      (JSON::JWK.new(oauth_jwt_public_key).merge(use: "sig", alg: oauth_jwt_algorithm) if oauth_jwt_public_key),
         | 
| 320 | 
            +
                      (JSON::JWK.new(oauth_jwt_jwe_public_key).merge(use: "enc", alg: oauth_jwt_jwe_algorithm) if oauth_jwt_jwe_public_key)
         | 
| 321 | 
            +
                    ].compact
         | 
| 322 | 
            +
                  end
         | 
| 323 | 
            +
             | 
| 324 | 
            +
                  # :nocov:
         | 
| 325 | 
            +
                elsif defined?(JWT)
         | 
| 326 | 
            +
             | 
| 327 | 
            +
                  # ruby-jwt
         | 
| 328 | 
            +
             | 
| 329 | 
            +
                  def jwk_import(data)
         | 
| 330 | 
            +
                    JWT::JWK.import(data).keypair
         | 
| 331 | 
            +
                  end
         | 
| 332 | 
            +
             | 
| 333 | 
            +
                  def jwt_encode(payload)
         | 
| 334 | 
            +
                    headers = {}
         | 
| 335 | 
            +
             | 
| 336 | 
            +
                    key = _jwt_key
         | 
| 337 | 
            +
             | 
| 338 | 
            +
                    if key.is_a?(OpenSSL::PKey::RSA)
         | 
| 339 | 
            +
                      jwk = JWT::JWK.new(_jwt_key)
         | 
| 340 | 
            +
                      headers[:kid] = jwk.kid
         | 
| 341 | 
            +
             | 
| 342 | 
            +
                      key = jwk.keypair
         | 
| 343 | 
            +
                    end
         | 
| 344 | 
            +
             | 
| 345 | 
            +
                    # Use the key and iat to create a unique key per request to prevent replay attacks
         | 
| 346 | 
            +
                    jti_raw = [key, payload[:iat]].join(":").to_s
         | 
| 347 | 
            +
                    jti = Digest::SHA256.hexdigest(jti_raw)
         | 
| 348 | 
            +
             | 
| 349 | 
            +
                    # @see JWT reserved claims - https://tools.ietf.org/html/draft-jones-json-web-token-07#page-7
         | 
| 350 | 
            +
                    payload[:jti] = jti
         | 
| 351 | 
            +
                    token = JWT.encode(payload, key, oauth_jwt_algorithm, headers)
         | 
| 352 | 
            +
             | 
| 353 | 
            +
                    if oauth_jwt_jwe_key
         | 
| 354 | 
            +
                      params = {
         | 
| 355 | 
            +
                        zip: "DEF",
         | 
| 356 | 
            +
                        copyright: oauth_jwt_jwe_copyright
         | 
| 357 | 
            +
                      }
         | 
| 358 | 
            +
                      params[:enc] = oauth_jwt_jwe_encryption_method if oauth_jwt_jwe_encryption_method
         | 
| 359 | 
            +
                      params[:alg] = oauth_jwt_jwe_algorithm if oauth_jwt_jwe_algorithm
         | 
| 360 | 
            +
                      token = JWE.encrypt(token, oauth_jwt_jwe_public_key || oauth_jwt_jwe_key, **params)
         | 
| 361 | 
            +
                    end
         | 
| 362 | 
            +
             | 
| 363 | 
            +
                    token
         | 
| 364 | 
            +
                  end
         | 
| 365 | 
            +
             | 
| 366 | 
            +
                  def jwt_decode(token, jws_key: oauth_jwt_public_key || _jwt_key, jws_algorithm: oauth_jwt_algorithm)
         | 
| 367 | 
            +
                    # decrypt jwe
         | 
| 368 | 
            +
                    token = JWE.decrypt(token, oauth_jwt_jwe_key) if oauth_jwt_jwe_key
         | 
| 369 | 
            +
             | 
| 370 | 
            +
                    # decode jwt
         | 
| 371 | 
            +
                    @jwt_token = if jws_key
         | 
| 372 | 
            +
                                   JWT.decode(token, jws_key, true, algorithms: [jws_algorithm]).first
         | 
| 373 | 
            +
                                 elsif !is_authorization_server? && auth_server_jwks_set
         | 
| 374 | 
            +
                                   algorithms = auth_server_jwks_set[:keys].select { |k| k[:use] == "sig" }.map { |k| k[:alg] }
         | 
| 375 | 
            +
                                   JWT.decode(token, nil, true, jwks: auth_server_jwks_set, algorithms: algorithms).first
         | 
| 376 | 
            +
                                 end
         | 
| 377 | 
            +
                  rescue JWT::DecodeError, JWT::JWKError
         | 
| 378 | 
            +
                    nil
         | 
| 379 | 
            +
                  end
         | 
| 380 | 
            +
             | 
| 381 | 
            +
                  def jwks_set
         | 
| 382 | 
            +
                    [
         | 
| 383 | 
            +
                      (JWT::JWK.new(oauth_jwt_public_key).export.merge(use: "sig", alg: oauth_jwt_algorithm) if oauth_jwt_public_key),
         | 
| 384 | 
            +
                      (JWT::JWK.new(oauth_jwt_jwe_public_key).export.merge(use: "enc", alg: oauth_jwt_jwe_algorithm) if oauth_jwt_jwe_public_key)
         | 
| 385 | 
            +
                    ].compact
         | 
| 386 | 
            +
                  end
         | 
| 387 | 
            +
                else
         | 
| 388 | 
            +
                  # :nocov:
         | 
| 389 | 
            +
                  def jwk_import(_data)
         | 
| 390 | 
            +
                    raise "#{__method__} is undefined, redefine it or require either \"jwt\" or \"json-jwt\""
         | 
| 391 | 
            +
                  end
         | 
| 392 | 
            +
             | 
| 393 | 
            +
                  def jwt_encode(_token)
         | 
| 394 | 
            +
                    raise "#{__method__} is undefined, redefine it or require either \"jwt\" or \"json-jwt\""
         | 
| 395 | 
            +
                  end
         | 
| 396 | 
            +
             | 
| 397 | 
            +
                  def jwt_decode(_token, **)
         | 
| 398 | 
            +
                    raise "#{__method__} is undefined, redefine it or require either \"jwt\" or \"json-jwt\""
         | 
| 399 | 
            +
                  end
         | 
| 400 | 
            +
             | 
| 401 | 
            +
                  def jwks_set
         | 
| 402 | 
            +
                    raise "#{__method__} is undefined, redefine it or require either \"jwt\" or \"json-jwt\""
         | 
| 403 | 
            +
                  end
         | 
| 404 | 
            +
                  # :nocov:
         | 
| 405 | 
            +
                end
         | 
| 406 | 
            +
             | 
| 407 | 
            +
                def validate_oauth_revoke_params
         | 
| 408 | 
            +
                  token_hint = param_or_nil("token_type_hint")
         | 
| 409 | 
            +
             | 
| 410 | 
            +
                  throw(:rodauth_error) if !token_hint || token_hint == "access_token"
         | 
| 411 | 
            +
             | 
| 412 | 
            +
                  super
         | 
| 413 | 
            +
                end
         | 
| 414 | 
            +
             | 
| 415 | 
            +
                route(:oauth_jwks) do |r|
         | 
| 416 | 
            +
                  r.get do
         | 
| 417 | 
            +
                    json_response_success({ keys: jwks_set })
         | 
| 418 | 
            +
                  end
         | 
| 419 | 
            +
                end
         | 
| 420 | 
            +
              end
         | 
| 421 | 
            +
            end
         |