lib/rodauth/features/oauth_jwt.rb in rodauth-oauth-0.0.6 vs lib/rodauth/features/oauth_jwt.rb in rodauth-oauth-0.1.0
- old
+ new
@@ -4,12 +4,15 @@
module Rodauth
Feature.define(:oauth_jwt) do
depends :oauth
- auth_value_method :oauth_jwt_token_issuer, "Example"
+ 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
+
auth_value_method :oauth_application_jws_jwk_column, nil
auth_value_method :oauth_jwt_key, nil
auth_value_method :oauth_jwt_public_key, nil
auth_value_method :oauth_jwt_algorithm, "HS256"
@@ -26,11 +29,12 @@
auth_value_method :invalid_request_object_message, "request object is invalid"
auth_value_methods(
:jwt_encode,
:jwt_decode,
- :jwks_set
+ :jwks_set,
+ :last_account_login_at
)
JWKS = OAuth::TtlStore.new
def require_oauth_authorization(*scopes)
@@ -43,10 +47,16 @@
authorization_required unless scopes.any? { |scope| token_scopes.include?(scope) }
end
private
+ unless method_defined?(:last_account_login_at)
+ def last_account_login_at
+ nil
+ end
+ end
+
def authorization_token
return @authorization_token if defined?(@authorization_token)
@authorization_token = begin
bearer_token = fetch_access_token
@@ -55,12 +65,12 @@
jwt_token = jwt_decode(bearer_token)
return unless jwt_token
- return if jwt_token["iss"] != oauth_jwt_token_issuer ||
- jwt_token["aud"] != oauth_jwt_audience ||
+ return if jwt_token["iss"] != (oauth_jwt_token_issuer || authorization_server_url) ||
+ (oauth_jwt_audience && jwt_token["aud"] != oauth_jwt_audience) ||
!jwt_token["sub"]
jwt_token
end
end
@@ -167,41 +177,62 @@
end
end
oauth_token = _generate_oauth_token(create_params)
+ claims = jwt_claims(oauth_token)
+
+ # one of the points of using jwt is avoiding database lookups, so we put here all relevant
+ # token data.
+ claims[:scope] = oauth_token[oauth_tokens_scopes_column]
+
+ token = jwt_encode(claims)
+
+ oauth_token[oauth_tokens_token_column] = token
+ oauth_token
+ end
+
+ def jwt_claims(oauth_token)
issued_at = Time.now.utc.to_i
- payload = {
- sub: oauth_token[oauth_tokens_account_id_column],
- iss: oauth_jwt_token_issuer, # issuer
+ claims = {
+ iss: (oauth_jwt_token_issuer || authorization_server_url), # 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,
-
- # one of the points of using jwt is avoiding database lookups, so we put here all relevant
- # token data.
- scope: oauth_token[oauth_tokens_scopes_column]
+ aud: (oauth_jwt_audience || oauth_application[oauth_applications_client_id_column])
}
- token = jwt_encode(payload)
+ claims[:auth_time] = last_account_login_at.utc.to_i if last_account_login_at
- oauth_token[oauth_tokens_token_column] = token
- oauth_token
+ claims
end
+ def jwt_subject(oauth_token)
+ case oauth_jwt_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 (#{oauth_jwt_subject_type})"
+ end
+ end
+
def oauth_token_by_token(token, *)
jwt_decode(token)
end
def json_token_introspect_payload(oauth_token)
@@ -226,21 +257,15 @@
end
def oauth_server_metadata_body(path)
metadata = super
metadata.merge! \
- jwks_uri: oauth_jwks_url,
+ jwks_uri: jwks_url,
token_endpoint_auth_signing_alg_values_supported: [oauth_jwt_algorithm]
metadata
end
- def token_from_application?(oauth_token, oauth_application)
- return super unless oauth_token["sub"] # naive check on whether it's a jwt token
-
- oauth_token["client_id"] == oauth_application[oauth_applications_client_id_column]
- end
-
def _jwt_key
@_jwt_key ||= oauth_jwt_key || (oauth_application[oauth_applications_client_secret_column] if oauth_application)
end
# Resource Server only!
@@ -410,10 +435,12 @@
throw(:rodauth_error) if !token_hint || token_hint == "access_token"
super
end
- route(:oauth_jwks) do |r|
+ route(:jwks) do |r|
+ next unless is_authorization_server?
+
r.get do
json_response_success({ keys: jwks_set })
end
end
end