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