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