lib/rodauth/features/oauth_jwt.rb in rodauth-oauth-0.0.3 vs lib/rodauth/features/oauth_jwt.rb in rodauth-oauth-0.0.4
- old
+ new
@@ -2,10 +2,13 @@
module Rodauth
Feature.define(:oauth_jwt) do
depends :oauth
+ auth_value_method :grant_type_param, "grant_type"
+ auth_value_method :assertion_param, "assertion"
+
auth_value_method :oauth_jwt_token_issuer, "Example"
auth_value_method :oauth_jwt_key, nil
auth_value_method :oauth_jwt_public_key, nil
auth_value_method :oauth_jwt_algorithm, "HS256"
@@ -21,41 +24,78 @@
auth_value_method :oauth_jwt_jwe_copyright, nil
auth_value_method :oauth_jwt_audience, nil
auth_value_methods(
- :generate_jti,
:jwt_encode,
- :jwt_decode
+ :jwt_decode,
+ :jwks_set
)
def require_oauth_authorization(*scopes)
authorization_required unless authorization_token
scopes << oauth_application_default_scope if scopes.empty?
- token_scopes = authorization_token["scopes"].split(",")
+ token_scopes = authorization_token["scope"].split(" ")
authorization_required unless scopes.any? { |scope| token_scopes.include?(scope) }
end
private
def authorization_token
return @authorization_token if defined?(@authorization_token)
- @authorization_token = begin
- value = request.get_header("HTTP_AUTHORIZATION").to_s
+ @authorization_token = jwt_decode(fetch_access_token)
+ end
- scheme, token = value.split(/ +/, 2)
+ # /token
- return unless scheme == "Bearer"
+ def before_token
+ # requset authentication optional for assertions
+ return if param(grant_type_param) == "urn:ietf:params:oauth:grant-type:jwt-bearer"
- jwt_decode(token)
+ super
+ end
+
+ def validate_oauth_token_params
+ if param(grant_type_param) == "urn:ietf:params:oauth:grant-type:jwt-bearer"
+ redirect_response_error("invalid_client") unless param_or_nil(assertion_param)
+ else
+ super
end
end
+ def create_oauth_token
+ if param(grant_type_param) == "urn:ietf:params:oauth:grant-type:jwt-bearer"
+ create_oauth_token_from_assertion
+ else
+ super
+ end
+ end
+
+ def create_oauth_token_from_assertion
+ claims = jwt_decode(param(assertion_param))
+
+ redirect_response_error("invalid_grant") unless claims
+
+ @oauth_application = db[oauth_applications_table].where(oauth_applications_client_id_column => claims["client_id"]).first
+
+ account = account_ds(claims["sub"]).first
+
+ redirect_response_error("invalid_client") unless oauth_application && account
+
+ create_params = {
+ oauth_tokens_account_id_column => claims["sub"],
+ oauth_tokens_oauth_application_id_column => oauth_application[oauth_applications_id_column],
+ oauth_tokens_scopes_column => claims["scope"]
+ }
+
+ generate_oauth_token(create_params, false)
+ end
+
def generate_oauth_token(params = {}, should_generate_refresh_token = true)
create_params = {
oauth_grants_expires_in_column => Time.now + oauth_token_expires_in
}.merge(params)
@@ -84,26 +124,65 @@
# 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.
- client_id: oauth_token[oauth_tokens_oauth_application_id_column],
+ 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.
- scopes: oauth_token[oauth_tokens_scopes_column]
+ scope: oauth_token[oauth_tokens_scopes_column].gsub(",", " ")
}
token = jwt_encode(payload)
oauth_token[oauth_tokens_token_column] = token
oauth_token
end
+ def oauth_token_by_token(token, *)
+ jwt_decode(token)
+ end
+
+ def json_token_introspect_payload(oauth_token)
+ return { active: false } unless oauth_token
+
+ return super unless oauth_token["sub"] # naive check on whether it's a jwt token
+
+ {
+ active: true,
+ scope: oauth_token["scope"],
+ client_id: oauth_token["client_id"],
+ # username
+ token_type: "access_token",
+ exp: oauth_token["exp"],
+ iat: oauth_token["iat"],
+ nbf: oauth_token["nbf"],
+ sub: oauth_token["sub"],
+ aud: oauth_token["aud"],
+ iss: oauth_token["iss"],
+ jti: oauth_token["jti"]
+ }
+ end
+
+ def oauth_server_metadata_body(path)
+ metadata = super
+ metadata.merge! \
+ jwks_uri: oauth_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]
end
if defined?(JSON::JWT)
@@ -128,20 +207,30 @@
end
jwt.to_s
end
def jwt_decode(token)
+ return @jwt_token if defined?(@jwt_token)
+
token = JSON::JWT.decode(token, oauth_jwt_jwe_key).plain_text if oauth_jwt_jwe_key
- if oauth_jwt_jwk_key
- jwk = JSON::JWK.new(oauth_jwt_jwk_key)
- JSON::JWT.decode(token, jwk)
- else
- JSON::JWT.decode(token, oauth_jwt_public_key || _jwt_key)
- end
+
+ @jwt_token = if oauth_jwt_jwk_key
+ jwk = JSON::JWK.new(oauth_jwt_jwk_public_key || oauth_jwt_jwk_key)
+ JSON::JWT.decode(token, jwk)
+ else
+ JSON::JWT.decode(token, oauth_jwt_public_key || _jwt_key)
+ end
rescue JSON::JWT::Exception
nil
end
+
+ def jwks_set
+ [
+ (JSON::JWK.new(oauth_jwt_jwk_public_key).merge(use: "sig", alg: oauth_jwt_jwk_algorithm) if oauth_jwt_jwk_public_key),
+ (JSON::JWK.new(oauth_jwt_jwe_public_key).merge(use: "enc", alg: oauth_jwt_jwe_algorithm) if oauth_jwt_jwe_public_key)
+ ].compact
+ end
# :nocov:
elsif defined?(JWT)
# ruby-jwt
@@ -161,11 +250,11 @@
[_jwt_key, oauth_jwt_algorithm]
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::MD5.hexdigest(jti_raw)
+ 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
token = JWT.encode(payload, key, algorithm, headers)
@@ -181,18 +270,20 @@
token
end
def jwt_decode(token)
+ return @jwt_token if defined?(@jwt_token)
+
# decrypt jwe
token = JWE.decrypt(token, oauth_jwt_jwe_key) if oauth_jwt_jwe_key
# decode jwt
headers = { algorithms: [oauth_jwt_algorithm] }
key = if oauth_jwt_jwk_key
- jwk_key = JWT::JWK.new(oauth_jwt_jwk_key)
+ jwk_key = JWT::JWK.new(oauth_jwt_jwk_public_key || oauth_jwt_jwk_key)
# JWK
# The jwk loader would fetch the set of JWKs from a trusted source
jwk_loader = lambda do |options|
@cached_keys = nil if options[:invalidate] # need to reload the keys
@cached_keys ||= { keys: [jwk_key.export] }
@@ -205,24 +296,40 @@
else
# JWS
# worst case scenario, the key is the application key
oauth_jwt_public_key || _jwt_key
end
- token, = JWT.decode(token, key, true, headers)
- token
+ @jwt_token, = JWT.decode(token, key, true, headers)
+ @jwt_token
rescue JWT::DecodeError
nil
end
+ def jwks_set
+ [
+ (JWT::JWK.new(oauth_jwt_jwk_public_key).export.merge(use: "sig", alg: oauth_jwt_jwk_algorithm) if oauth_jwt_jwk_public_key),
+ (JWT::JWK.new(oauth_jwt_jwe_public_key).export.merge(use: "enc", alg: oauth_jwt_jwe_algorithm) if oauth_jwt_jwe_public_key)
+ ].compact
+ end
else
# :nocov:
def jwt_encode(_token)
raise "#{__method__} is undefined, redefine it or require either \"jwt\" or \"json-jwt\""
end
def jwt_decode(_token)
raise "#{__method__} is undefined, redefine it or require either \"jwt\" or \"json-jwt\""
end
+
+ def jwks_set
+ raise "#{__method__} is undefined, redefine it or require either \"jwt\" or \"json-jwt\""
+ end
# :nocov:
+ end
+
+ route(:oauth_jwks) do |r|
+ r.get do
+ json_response_success(jwks_set)
+ end
end
end
end