lib/rodauth/features/oauth_jwt.rb in rodauth-oauth-0.4.3 vs lib/rodauth/features/oauth_jwt.rb in rodauth-oauth-0.5.0

- old
+ new

@@ -6,10 +6,12 @@ Feature.define(:oauth_jwt) do depends :oauth JWKS = OAuth::TtlStore.new + # Recommended to have hmac_secret as well + auth_value_method :oauth_jwt_subject_type, "public" # public, pairwise auth_value_method :oauth_jwt_subject_secret, nil # salt for pairwise generation auth_value_method :oauth_jwt_token_issuer, nil @@ -36,11 +38,12 @@ auth_value_methods( :jwt_encode, :jwt_decode, :jwks_set, - :last_account_login_at + :last_account_login_at, + :generate_jti ) route(:jwks) do |r| next unless is_authorization_server? @@ -65,10 +68,14 @@ def last_account_login_at nil end end + def issuer + @issuer ||= oauth_jwt_token_issuer || authorization_server_url + end + def authorization_token return @authorization_token if defined?(@authorization_token) @authorization_token = begin bearer_token = fetch_access_token @@ -77,11 +84,11 @@ jwt_token = jwt_decode(bearer_token) return unless jwt_token - return if jwt_token["iss"] != (oauth_jwt_token_issuer || authorization_server_url) || + return if jwt_token["iss"] != issuer || (oauth_jwt_audience && jwt_token["aud"] != oauth_jwt_audience) || !jwt_token["sub"] jwt_token end @@ -103,11 +110,11 @@ jwk = JSON.parse(jwk, symbolize_names: true) if jwk && jwk.is_a?(String) else redirect_response_error("invalid_request_object") end - claims = jwt_decode(request_object, jws_key: jwk_import(jws_jwk), jws_algorithm: jwk[:alg]) + claims = jwt_decode(request_object, jws_key: jwk_import(jws_jwk), jws_algorithm: jwk[:alg], verify_jti: false) redirect_response_error("invalid_request_object") unless claims # If signed, the Authorization Request # Object SHOULD contain the Claims "iss" (issuer) and "aud" (audience) @@ -116,11 +123,11 @@ # the Authorization Server (AS) "issuer" as defined in RFC8414 # [RFC8414]. claims.delete("iss") audience = claims.delete("aud") - redirect_response_error("invalid_request_object") if audience && audience != authorization_server_url + redirect_response_error("invalid_request_object") if audience && audience != issuer claims.each do |k, v| request.params[k.to_s] = v end @@ -207,11 +214,11 @@ def jwt_claims(oauth_token) issued_at = Time.now.to_i claims = { - iss: (oauth_jwt_token_issuer || authorization_server_url), # issuer + 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" @@ -315,18 +322,36 @@ [JSON.parse(response.body, symbolize_names: true), ttl] end end + 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 + + def verify_jti(jti, claims) + generate_jti(claims) == jti + end + + def verify_aud(aud, claims) + aud == (oauth_jwt_audience || claims["client_id"]) + end + if defined?(JSON::JWT) def jwk_import(data) JSON::JWK.new(data) end # json-jwt def jwt_encode(payload) + payload[:jti] = generate_jti(payload) jwt = JSON::JWT.new(payload) jwk = JSON::JWK.new(_jwt_key) jwt = jwt.sign(jwk, oauth_jwt_algorithm) jwt.kid = jwk.thumbprint @@ -338,22 +363,38 @@ oauth_jwt_jwe_encryption_method.to_sym) end jwt.to_s end - def jwt_decode(token, jws_key: oauth_jwt_public_key || _jwt_key, **) + def jwt_decode( + token, + jws_key: oauth_jwt_public_key || _jwt_key, + verify_claims: true, + verify_jti: true, + ** + ) token = JSON::JWT.decode(token, oauth_jwt_jwe_key).plain_text if oauth_jwt_jwe_key - if is_authorization_server? - if oauth_jwt_legacy_public_key - JSON::JWT.decode(token, JSON::JWK::Set.new({ keys: jwks_set })) - 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)) + claims = if is_authorization_server? + if oauth_jwt_legacy_public_key + JSON::JWT.decode(token, JSON::JWK::Set.new({ keys: jwks_set })) + 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 + + if verify_claims && !(claims[:iss] == issuer && + verify_aud(claims[:aud], claims) && + (!claims[:iat] || Time.at(claims[:iat]) > (Time.now - oauth_token_expires_in)) && + (!claims[:exp] || Time.at(claims[:exp]) > Time.now) && + (!verify_jti || verify_jti(claims[:jti], claims))) + return end + + claims rescue JSON::JWT::Exception nil end def jwks_set @@ -382,16 +423,12 @@ headers[:kid] = jwk.kid key = jwk.keypair end - # Use the key and iat to create a unique key per request to prevent replay attacks - jti_raw = [key, payload[:iat]].join(":").to_s - jti = Digest::SHA256.hexdigest(jti_raw) - # @see JWT reserved claims - https://tools.ietf.org/html/draft-jones-json-web-token-07#page-7 - payload[:jti] = jti + payload[:jti] = generate_jti(payload) token = JWT.encode(payload, key, oauth_jwt_algorithm, headers) if oauth_jwt_jwe_key params = { zip: "DEF", @@ -403,24 +440,57 @@ end token end - def jwt_decode(token, jws_key: oauth_jwt_public_key || _jwt_key, jws_algorithm: oauth_jwt_algorithm) + def jwt_decode( + token, + jws_key: oauth_jwt_public_key || _jwt_key, + jws_algorithm: oauth_jwt_algorithm, + verify_claims: true, + verify_jti: true + ) # decrypt jwe token = JWE.decrypt(token, oauth_jwt_jwe_key) if oauth_jwt_jwe_key + + # verifying the JWT implies verifying: + # + # issuer: check that server generated the token + # aud: check the audience field (client is who he says he is) + # iat: check that the token didn't expire + # + # subject can't be verified automatically without having access to the account id, + # which we don't because that's the whole point. + # + verify_claims_params = if verify_claims + { + verify_iss: true, + iss: issuer, + # can't use stock aud verification, as it's dependent on the client application id + verify_aud: false, + verify_jti: (verify_jti ? method(:verify_jti) : false), + verify_iat: true + } + else + {} + end + # decode jwt - if is_authorization_server? - if oauth_jwt_legacy_public_key - algorithms = jwks_set.select { |k| k[:use] == "sig" }.map { |k| k[:alg] } - JWT.decode(token, nil, true, jwks: { keys: jwks_set }, algorithms: algorithms).first - elsif jws_key - JWT.decode(token, jws_key, true, algorithms: [jws_algorithm]).first - end - elsif (jwks = auth_server_jwks_set) - algorithms = jwks[:keys].select { |k| k[:use] == "sig" }.map { |k| k[:alg] } - JWT.decode(token, nil, true, jwks: jwks, algorithms: algorithms).first - end + claims = if is_authorization_server? + if oauth_jwt_legacy_public_key + algorithms = jwks_set.select { |k| k[:use] == "sig" }.map { |k| k[:alg] } + JWT.decode(token, nil, true, jwks: { keys: jwks_set }, algorithms: algorithms, **verify_claims_params).first + elsif jws_key + JWT.decode(token, jws_key, true, algorithms: [jws_algorithm], **verify_claims_params).first + end + elsif (jwks = auth_server_jwks_set) + algorithms = jwks[:keys].select { |k| k[:use] == "sig" }.map { |k| k[:alg] } + JWT.decode(token, nil, true, jwks: jwks, algorithms: algorithms, **verify_claims_params).first + end + + return if verify_claims && !verify_aud(claims["aud"], claims) + + claims rescue JWT::DecodeError, JWT::JWKError nil end def jwks_set