token_authority 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +23 -0
- data/README.md +199 -7
- data/app/controllers/concerns/token_authority/client_authentication.rb +141 -0
- data/app/controllers/concerns/token_authority/controller_event_logging.rb +98 -0
- data/app/controllers/concerns/token_authority/initial_access_token_authentication.rb +35 -0
- data/app/controllers/concerns/token_authority/token_authentication.rb +128 -0
- data/app/controllers/token_authority/authorization_grants_controller.rb +119 -0
- data/app/controllers/token_authority/authorizations_controller.rb +105 -0
- data/app/controllers/token_authority/clients_controller.rb +99 -0
- data/app/controllers/token_authority/metadata_controller.rb +12 -0
- data/app/controllers/token_authority/resource_metadata_controller.rb +12 -0
- data/app/controllers/token_authority/sessions_controller.rb +228 -0
- data/app/helpers/token_authority/authorization_grants_helper.rb +27 -0
- data/app/models/concerns/token_authority/claim_validatable.rb +95 -0
- data/app/models/concerns/token_authority/event_logging.rb +144 -0
- data/app/models/concerns/token_authority/resourceable.rb +111 -0
- data/app/models/concerns/token_authority/scopeable.rb +105 -0
- data/app/models/concerns/token_authority/session_creatable.rb +101 -0
- data/app/models/token_authority/access_token.rb +127 -0
- data/app/models/token_authority/access_token_request.rb +193 -0
- data/app/models/token_authority/authorization_grant.rb +119 -0
- data/app/models/token_authority/authorization_request.rb +276 -0
- data/app/models/token_authority/authorization_server_metadata.rb +101 -0
- data/app/models/token_authority/client.rb +263 -0
- data/app/models/token_authority/client_id_resolver.rb +114 -0
- data/app/models/token_authority/client_metadata_document.rb +164 -0
- data/app/models/token_authority/client_metadata_document_cache.rb +33 -0
- data/app/models/token_authority/client_metadata_document_fetcher.rb +266 -0
- data/app/models/token_authority/client_registration_request.rb +214 -0
- data/app/models/token_authority/client_registration_response.rb +58 -0
- data/app/models/token_authority/jwks_cache.rb +37 -0
- data/app/models/token_authority/jwks_fetcher.rb +70 -0
- data/app/models/token_authority/protected_resource_metadata.rb +74 -0
- data/app/models/token_authority/refresh_token.rb +110 -0
- data/app/models/token_authority/refresh_token_request.rb +116 -0
- data/app/models/token_authority/session.rb +193 -0
- data/app/models/token_authority/software_statement.rb +70 -0
- data/app/views/token_authority/authorization_grants/new.html.erb +25 -0
- data/app/views/token_authority/client_error.html.erb +8 -0
- data/config/locales/token_authority.en.yml +248 -0
- data/config/routes.rb +29 -0
- data/lib/generators/token_authority/install/install_generator.rb +61 -0
- data/lib/generators/token_authority/install/templates/create_token_authority_tables.rb.erb +116 -0
- data/lib/generators/token_authority/install/templates/token_authority.rb +247 -0
- data/lib/token_authority/configuration.rb +397 -0
- data/lib/token_authority/engine.rb +34 -0
- data/lib/token_authority/errors.rb +221 -0
- data/lib/token_authority/instrumentation.rb +80 -0
- data/lib/token_authority/instrumentation_log_subscriber.rb +62 -0
- data/lib/token_authority/json_web_token.rb +78 -0
- data/lib/token_authority/log_event_subscriber.rb +43 -0
- data/lib/token_authority/routing/constraints.rb +71 -0
- data/lib/token_authority/routing/routes.rb +39 -0
- data/lib/token_authority/version.rb +4 -1
- data/lib/token_authority.rb +30 -1
- metadata +65 -5
- data/app/assets/stylesheets/token_authority/application.css +0 -15
- data/app/controllers/token_authority/application_controller.rb +0 -4
- data/app/helpers/token_authority/application_helper.rb +0 -4
- data/app/views/layouts/token_authority/application.html.erb +0 -17
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TokenAuthority
|
|
4
|
+
##
|
|
5
|
+
# Controller for granting authorization to clients.
|
|
6
|
+
#
|
|
7
|
+
# Inherits from the controller configured via TokenAuthority.config.authenticatable_controller.
|
|
8
|
+
# The authenticatable controller must implement:
|
|
9
|
+
# - authenticate_user! (before_action that ensures user is logged in)
|
|
10
|
+
# - current_user (returns the currently authenticated user)
|
|
11
|
+
#
|
|
12
|
+
# For Devise users, these methods are already available on ApplicationController.
|
|
13
|
+
# For other authentication systems, implement these methods on your authenticatable controller.
|
|
14
|
+
class AuthorizationGrantsController < TokenAuthority.config.authenticatable_controller.constantize
|
|
15
|
+
include TokenAuthority::ControllerEventLogging
|
|
16
|
+
|
|
17
|
+
layout -> { TokenAuthority.config.consent_page_layout }
|
|
18
|
+
|
|
19
|
+
before_action :authenticate_user!
|
|
20
|
+
before_action :set_authorization_request
|
|
21
|
+
before_action :set_token_authority_client
|
|
22
|
+
|
|
23
|
+
rescue_from TokenAuthority::InvalidRedirectUrlError do |error|
|
|
24
|
+
session.delete(:token_authority_internal_state)
|
|
25
|
+
render "token_authority/client_error",
|
|
26
|
+
layout: TokenAuthority.config.error_page_layout,
|
|
27
|
+
status: :bad_request,
|
|
28
|
+
locals: {error_class: error.class, error_message: error.message}
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def new
|
|
32
|
+
notify_event("authorization.consent.shown",
|
|
33
|
+
client_id: @token_authority_client.public_id,
|
|
34
|
+
client_name: @token_authority_client.name,
|
|
35
|
+
requested_scopes: @authorization_request.scope)
|
|
36
|
+
|
|
37
|
+
render :new, locals: {
|
|
38
|
+
client_name: @token_authority_client.name,
|
|
39
|
+
resources: @authorization_request.resources,
|
|
40
|
+
scopes: @authorization_request.scope
|
|
41
|
+
}
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def create
|
|
45
|
+
state = @authorization_request.state
|
|
46
|
+
|
|
47
|
+
unless ActiveModel::Type::Boolean.new.cast(params[:approve])
|
|
48
|
+
notify_event("authorization.consent.denied",
|
|
49
|
+
client_id: @token_authority_client.public_id)
|
|
50
|
+
|
|
51
|
+
redirect_to_client(params_for_redirect: {error: "access_denied", state:}) and return
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
notify_event("authorization.consent.granted",
|
|
55
|
+
client_id: @token_authority_client.public_id,
|
|
56
|
+
granted_scopes: @authorization_request.scope)
|
|
57
|
+
|
|
58
|
+
grant = @token_authority_client.new_authorization_grant(
|
|
59
|
+
user: current_user,
|
|
60
|
+
challenge_params: {
|
|
61
|
+
code_challenge: @authorization_request.code_challenge,
|
|
62
|
+
code_challenge_method: @authorization_request.code_challenge_method,
|
|
63
|
+
redirect_uri: @authorization_request.redirect_uri,
|
|
64
|
+
resources: @authorization_request.resources,
|
|
65
|
+
scopes: @authorization_request.scope
|
|
66
|
+
}
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
if grant.persisted?
|
|
70
|
+
notify_event("authorization.grant.created",
|
|
71
|
+
grant_id: grant.public_id,
|
|
72
|
+
client_id: @token_authority_client.public_id,
|
|
73
|
+
expires_at: grant.expires_at&.iso8601,
|
|
74
|
+
scopes: grant.scopes)
|
|
75
|
+
|
|
76
|
+
redirect_to_client(params_for_redirect: {code: grant.public_id, state:}) and return
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
redirect_to_client(params_for_redirect: {error: "invalid_request", state:})
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
def set_authorization_request
|
|
85
|
+
internal_state = session[:token_authority_internal_state]
|
|
86
|
+
|
|
87
|
+
if internal_state.blank?
|
|
88
|
+
render_state_error("Authorization state not found")
|
|
89
|
+
return
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
@authorization_request = TokenAuthority::AuthorizationRequest.from_internal_state_token(internal_state)
|
|
93
|
+
rescue JWT::DecodeError, JWT::ExpiredSignature
|
|
94
|
+
clear_internal_state
|
|
95
|
+
render_state_error("Invalid or expired authorization state")
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def set_token_authority_client
|
|
99
|
+
@token_authority_client = @authorization_request.token_authority_client
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def redirect_to_client(params_for_redirect:)
|
|
103
|
+
clear_internal_state
|
|
104
|
+
url = @token_authority_client.url_for_redirect(params: params_for_redirect.compact)
|
|
105
|
+
redirect_to url, allow_other_host: true
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def clear_internal_state
|
|
109
|
+
session.delete(:token_authority_internal_state)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def render_state_error(message)
|
|
113
|
+
render "token_authority/client_error",
|
|
114
|
+
layout: TokenAuthority.config.error_page_layout,
|
|
115
|
+
status: :bad_request,
|
|
116
|
+
locals: {error_class: "InvalidStateError", error_message: message}
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TokenAuthority
|
|
4
|
+
# Handles OAuth 2.1 authorization requests (GET /authorize).
|
|
5
|
+
#
|
|
6
|
+
# This controller implements the initial step of the authorization code flow.
|
|
7
|
+
# It validates the authorization request parameters, authenticates the client,
|
|
8
|
+
# and redirects to the consent screen if everything is valid.
|
|
9
|
+
#
|
|
10
|
+
# The controller emits structured events for monitoring authorization requests,
|
|
11
|
+
# validation failures, and error conditions. Error responses follow OAuth 2.1
|
|
12
|
+
# specifications, redirecting to the client's redirect_uri when possible.
|
|
13
|
+
#
|
|
14
|
+
# @example Authorization request
|
|
15
|
+
# GET /authorize?client_id=abc123
|
|
16
|
+
# &redirect_uri=https://app.example.com/callback
|
|
17
|
+
# &response_type=code
|
|
18
|
+
# &state=xyz
|
|
19
|
+
# &code_challenge=E9Melhoa...
|
|
20
|
+
# &code_challenge_method=S256
|
|
21
|
+
# &scope=read+write
|
|
22
|
+
#
|
|
23
|
+
# @since 0.2.0
|
|
24
|
+
class AuthorizationsController < ActionController::Base
|
|
25
|
+
include TokenAuthority::ClientAuthentication
|
|
26
|
+
include TokenAuthority::ControllerEventLogging
|
|
27
|
+
|
|
28
|
+
before_action :authenticate_client
|
|
29
|
+
|
|
30
|
+
rescue_from TokenAuthority::InvalidRedirectUrlError do |error|
|
|
31
|
+
render "token_authority/client_error",
|
|
32
|
+
layout: TokenAuthority.config.error_page_layout,
|
|
33
|
+
status: :bad_request,
|
|
34
|
+
locals: {error_class: error.class, error_message: error.message}
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def authorize
|
|
38
|
+
state = params[:state]
|
|
39
|
+
resources = Array(params[:resource]).presence || []
|
|
40
|
+
|
|
41
|
+
notify_event("authorization.request.received",
|
|
42
|
+
client_id: params[:client_id],
|
|
43
|
+
client_type: @token_authority_client&.client_type,
|
|
44
|
+
redirect_uri: params[:redirect_uri],
|
|
45
|
+
has_pkce: params[:code_challenge].present?,
|
|
46
|
+
requested_scopes: params[:scope],
|
|
47
|
+
requested_resources: resources)
|
|
48
|
+
|
|
49
|
+
authorization_request = @token_authority_client.new_authorization_request(
|
|
50
|
+
client_id: params[:client_id],
|
|
51
|
+
code_challenge: params[:code_challenge],
|
|
52
|
+
code_challenge_method: params[:code_challenge_method],
|
|
53
|
+
redirect_uri: params[:redirect_uri],
|
|
54
|
+
response_type: params[:response_type],
|
|
55
|
+
state:,
|
|
56
|
+
resources:,
|
|
57
|
+
scope: params[:scope]
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
if authorization_request.valid?
|
|
61
|
+
notify_event("authorization.request.validated",
|
|
62
|
+
client_id: params[:client_id],
|
|
63
|
+
validated_scopes: authorization_request.scope,
|
|
64
|
+
validated_resources: authorization_request.resources)
|
|
65
|
+
|
|
66
|
+
session[:token_authority_internal_state] = authorization_request.to_internal_state_token
|
|
67
|
+
redirect_to new_authorization_grant_path
|
|
68
|
+
elsif authorization_request.errors.where(:redirect_uri).any?
|
|
69
|
+
notify_event("authorization.request.failed",
|
|
70
|
+
client_id: params[:client_id],
|
|
71
|
+
error_type: "invalid_redirect_uri",
|
|
72
|
+
validation_errors: authorization_request.errors.full_messages)
|
|
73
|
+
|
|
74
|
+
head :bad_request and return
|
|
75
|
+
elsif authorization_request.errors.where(:resources).any?
|
|
76
|
+
notify_event("authorization.request.failed",
|
|
77
|
+
client_id: params[:client_id],
|
|
78
|
+
error_type: "invalid_target",
|
|
79
|
+
validation_errors: authorization_request.errors.full_messages)
|
|
80
|
+
|
|
81
|
+
params_for_redirect = {error: :invalid_target, state:}.compact
|
|
82
|
+
url = @token_authority_client.url_for_redirect(params: params_for_redirect.compact)
|
|
83
|
+
redirect_to url, allow_other_host: true
|
|
84
|
+
elsif authorization_request.errors.where(:scope).any?
|
|
85
|
+
notify_event("authorization.request.failed",
|
|
86
|
+
client_id: params[:client_id],
|
|
87
|
+
error_type: "invalid_scope",
|
|
88
|
+
validation_errors: authorization_request.errors.full_messages)
|
|
89
|
+
|
|
90
|
+
params_for_redirect = {error: :invalid_scope, state:}.compact
|
|
91
|
+
url = @token_authority_client.url_for_redirect(params: params_for_redirect.compact)
|
|
92
|
+
redirect_to url, allow_other_host: true
|
|
93
|
+
else
|
|
94
|
+
notify_event("authorization.request.failed",
|
|
95
|
+
client_id: params[:client_id],
|
|
96
|
+
error_type: "invalid_request",
|
|
97
|
+
validation_errors: authorization_request.errors.full_messages)
|
|
98
|
+
|
|
99
|
+
params_for_redirect = {error: :invalid_request, state:}.compact
|
|
100
|
+
url = @token_authority_client.url_for_redirect(params: params_for_redirect.compact)
|
|
101
|
+
redirect_to url, allow_other_host: true
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TokenAuthority
|
|
4
|
+
##
|
|
5
|
+
# Controller for dynamic client registration (RFC 7591)
|
|
6
|
+
class ClientsController < ActionController::API
|
|
7
|
+
include TokenAuthority::InitialAccessTokenAuthentication
|
|
8
|
+
include TokenAuthority::ControllerEventLogging
|
|
9
|
+
|
|
10
|
+
rescue_from TokenAuthority::InvalidClientMetadataError do |error|
|
|
11
|
+
notify_event("client.registration.failed",
|
|
12
|
+
error_type: "invalid_client_metadata",
|
|
13
|
+
validation_errors: [error.message])
|
|
14
|
+
|
|
15
|
+
render_registration_error("invalid_client_metadata", error.message)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
rescue_from TokenAuthority::InvalidSoftwareStatementError do |error|
|
|
19
|
+
notify_event("client.registration.failed",
|
|
20
|
+
error_type: "invalid_software_statement",
|
|
21
|
+
validation_errors: [error.message])
|
|
22
|
+
|
|
23
|
+
render_registration_error("invalid_software_statement", error.message)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
rescue_from TokenAuthority::UnapprovedSoftwareStatementError do |error|
|
|
27
|
+
notify_event("client.registration.failed",
|
|
28
|
+
error_type: "unapproved_software_statement",
|
|
29
|
+
validation_errors: [error.message])
|
|
30
|
+
|
|
31
|
+
render_registration_error("unapproved_software_statement", error.message)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
rescue_from TokenAuthority::InvalidInitialAccessTokenError do
|
|
35
|
+
notify_event("client.registration.failed",
|
|
36
|
+
error_type: "invalid_token",
|
|
37
|
+
validation_errors: ["Initial access token is invalid or missing"])
|
|
38
|
+
|
|
39
|
+
render_registration_error("invalid_token", "Initial access token is invalid or missing", status: :unauthorized)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
rescue_from ActiveRecord::RecordInvalid do |error|
|
|
43
|
+
notify_event("client.registration.failed",
|
|
44
|
+
error_type: "invalid_client_metadata",
|
|
45
|
+
validation_errors: error.record.errors.full_messages)
|
|
46
|
+
|
|
47
|
+
render_registration_error("invalid_client_metadata", error.record.errors.full_messages.join(", "))
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def create
|
|
51
|
+
request = ClientRegistrationRequest.new(registration_params)
|
|
52
|
+
|
|
53
|
+
if request.valid?
|
|
54
|
+
client = request.create_client!
|
|
55
|
+
|
|
56
|
+
notify_event("client.registration.completed",
|
|
57
|
+
client_id: client.public_id,
|
|
58
|
+
client_name: client.name,
|
|
59
|
+
client_type: client.client_type,
|
|
60
|
+
grant_types: client.grant_types)
|
|
61
|
+
|
|
62
|
+
render json: ClientRegistrationResponse.new(client:).to_h, status: :created
|
|
63
|
+
else
|
|
64
|
+
notify_event("client.registration.failed",
|
|
65
|
+
error_type: "invalid_client_metadata",
|
|
66
|
+
validation_errors: request.errors.full_messages)
|
|
67
|
+
|
|
68
|
+
render_registration_error("invalid_client_metadata", request.errors.full_messages.join(", "))
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def registration_params
|
|
75
|
+
params.permit(
|
|
76
|
+
:token_endpoint_auth_method,
|
|
77
|
+
:client_name,
|
|
78
|
+
:client_uri,
|
|
79
|
+
:logo_uri,
|
|
80
|
+
:tos_uri,
|
|
81
|
+
:policy_uri,
|
|
82
|
+
:scope,
|
|
83
|
+
:jwks_uri,
|
|
84
|
+
:software_id,
|
|
85
|
+
:software_version,
|
|
86
|
+
:software_statement,
|
|
87
|
+
redirect_uris: [],
|
|
88
|
+
grant_types: [],
|
|
89
|
+
response_types: [],
|
|
90
|
+
contacts: [],
|
|
91
|
+
jwks: {}
|
|
92
|
+
)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def render_registration_error(error, error_description, status: :bad_request)
|
|
96
|
+
render json: {error:, error_description:}, status:
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TokenAuthority
|
|
4
|
+
##
|
|
5
|
+
# Controller for RFC 8414 OAuth 2.0 Authorization Server Metadata
|
|
6
|
+
class MetadataController < ActionController::API
|
|
7
|
+
def show
|
|
8
|
+
metadata = AuthorizationServerMetadata.new(mount_path: params[:mount_path])
|
|
9
|
+
render json: metadata.to_h
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TokenAuthority
|
|
4
|
+
##
|
|
5
|
+
# Controller for RFC 9728 OAuth 2.0 Protected Resource Metadata
|
|
6
|
+
class ResourceMetadataController < ActionController::API
|
|
7
|
+
def show
|
|
8
|
+
metadata = ProtectedResourceMetadata.new(mount_path: params[:mount_path])
|
|
9
|
+
render json: metadata.to_h
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TokenAuthority
|
|
4
|
+
##
|
|
5
|
+
# Controller for issuing access and refresh tokens.
|
|
6
|
+
class SessionsController < ActionController::API
|
|
7
|
+
include TokenAuthority::ClientAuthentication
|
|
8
|
+
include TokenAuthority::ControllerEventLogging
|
|
9
|
+
|
|
10
|
+
before_action :set_authorization_grant, only: :token
|
|
11
|
+
before_action :authenticate_client, except: :unsupported_grant_type
|
|
12
|
+
|
|
13
|
+
rescue_from TokenAuthority::InvalidGrantError do
|
|
14
|
+
notify_event("token.exchange.failed",
|
|
15
|
+
client_id: params[:client_id],
|
|
16
|
+
error_type: "invalid_grant",
|
|
17
|
+
validation_errors: ["Authorization grant is invalid or expired"])
|
|
18
|
+
|
|
19
|
+
render_token_request_error(error: "invalid_grant")
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
rescue_from TokenAuthority::ServerError do |error|
|
|
23
|
+
Rails.logger.error(error.message)
|
|
24
|
+
render_token_request_error(error: "server_error", status: :internal_server_error)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def token
|
|
28
|
+
resources = Array(params[:resource]).presence || []
|
|
29
|
+
|
|
30
|
+
notify_event("token.exchange.requested",
|
|
31
|
+
client_id: params[:client_id],
|
|
32
|
+
grant_id: @authorization_grant&.public_id,
|
|
33
|
+
has_code_verifier: params[:code_verifier].present?)
|
|
34
|
+
|
|
35
|
+
access_token_request = TokenAuthority::AccessTokenRequest.new(
|
|
36
|
+
token_authority_authorization_grant: @authorization_grant,
|
|
37
|
+
code_verifier: params[:code_verifier],
|
|
38
|
+
redirect_uri: params[:redirect_uri],
|
|
39
|
+
resources:,
|
|
40
|
+
scope: params[:scope]
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
if access_token_request.valid?
|
|
44
|
+
access_token, refresh_token, expiration, scope, token_authority_session = @authorization_grant.redeem(
|
|
45
|
+
resources: access_token_request.effective_resources,
|
|
46
|
+
scopes: access_token_request.effective_scopes
|
|
47
|
+
).deconstruct
|
|
48
|
+
|
|
49
|
+
notify_event("token.exchange.completed",
|
|
50
|
+
client_id: params[:client_id],
|
|
51
|
+
session_id: token_authority_session&.id,
|
|
52
|
+
expires_in: expiration)
|
|
53
|
+
|
|
54
|
+
response_body = {access_token:, refresh_token:, token_type: "bearer", expires_in: expiration, scope:}.compact
|
|
55
|
+
render json: response_body
|
|
56
|
+
elsif access_token_request.errors.where(:resources).any?
|
|
57
|
+
notify_event("token.exchange.failed",
|
|
58
|
+
client_id: params[:client_id],
|
|
59
|
+
error_type: "invalid_target",
|
|
60
|
+
validation_errors: access_token_request.errors.full_messages)
|
|
61
|
+
|
|
62
|
+
render_token_request_error(error: "invalid_target")
|
|
63
|
+
elsif access_token_request.errors.where(:scope).any?
|
|
64
|
+
notify_event("token.exchange.failed",
|
|
65
|
+
client_id: params[:client_id],
|
|
66
|
+
error_type: "invalid_scope",
|
|
67
|
+
validation_errors: access_token_request.errors.full_messages)
|
|
68
|
+
|
|
69
|
+
render_token_request_error(error: "invalid_scope")
|
|
70
|
+
else
|
|
71
|
+
notify_event("token.exchange.failed",
|
|
72
|
+
client_id: params[:client_id],
|
|
73
|
+
error_type: "invalid_request",
|
|
74
|
+
validation_errors: access_token_request.errors.full_messages)
|
|
75
|
+
|
|
76
|
+
render_token_request_error(error: "invalid_request")
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def refresh
|
|
81
|
+
token = TokenAuthority::RefreshToken.from_token(params[:refresh_token])
|
|
82
|
+
resources = Array(params[:resource]).presence || []
|
|
83
|
+
|
|
84
|
+
refresh_token_request = TokenAuthority::RefreshTokenRequest.new(
|
|
85
|
+
token:,
|
|
86
|
+
client_id: params[:client_id],
|
|
87
|
+
resources:,
|
|
88
|
+
scope: params[:scope]
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
old_session = refresh_token_request.token_authority_session
|
|
92
|
+
|
|
93
|
+
notify_event("token.refresh.requested",
|
|
94
|
+
client_id: params[:client_id],
|
|
95
|
+
session_id: old_session&.id)
|
|
96
|
+
|
|
97
|
+
if refresh_token_request.valid?
|
|
98
|
+
access_token, refresh_token, expiration, scope, new_session = old_session.refresh(
|
|
99
|
+
token:,
|
|
100
|
+
client_id: refresh_token_request.resolved_client_id,
|
|
101
|
+
resources: refresh_token_request.effective_resources,
|
|
102
|
+
scopes: refresh_token_request.effective_scopes
|
|
103
|
+
).deconstruct
|
|
104
|
+
|
|
105
|
+
notify_event("token.refresh.completed",
|
|
106
|
+
client_id: params[:client_id],
|
|
107
|
+
old_session_id: old_session&.id,
|
|
108
|
+
new_session_id: new_session&.id)
|
|
109
|
+
|
|
110
|
+
response_body = {access_token:, refresh_token:, token_type: "bearer", expires_in: expiration, scope:}.compact
|
|
111
|
+
render json: response_body
|
|
112
|
+
elsif refresh_token_request.errors.where(:resources).any?
|
|
113
|
+
notify_event("token.refresh.failed",
|
|
114
|
+
client_id: params[:client_id],
|
|
115
|
+
error_type: "invalid_target",
|
|
116
|
+
validation_errors: refresh_token_request.errors.full_messages)
|
|
117
|
+
|
|
118
|
+
render_token_request_error(error: "invalid_target")
|
|
119
|
+
elsif refresh_token_request.errors.where(:scope).any?
|
|
120
|
+
notify_event("token.refresh.failed",
|
|
121
|
+
client_id: params[:client_id],
|
|
122
|
+
error_type: "invalid_scope",
|
|
123
|
+
validation_errors: refresh_token_request.errors.full_messages)
|
|
124
|
+
|
|
125
|
+
render_token_request_error(error: "invalid_scope")
|
|
126
|
+
else
|
|
127
|
+
notify_event("token.refresh.failed",
|
|
128
|
+
client_id: params[:client_id],
|
|
129
|
+
error_type: "invalid_request",
|
|
130
|
+
validation_errors: refresh_token_request.errors.full_messages)
|
|
131
|
+
|
|
132
|
+
render_token_request_error(error: "invalid_request")
|
|
133
|
+
end
|
|
134
|
+
rescue JWT::DecodeError
|
|
135
|
+
notify_event("token.refresh.failed",
|
|
136
|
+
client_id: params[:client_id],
|
|
137
|
+
error_type: "invalid_request",
|
|
138
|
+
validation_errors: ["Invalid refresh token format"])
|
|
139
|
+
|
|
140
|
+
render_token_request_error(error: "invalid_request")
|
|
141
|
+
rescue TokenAuthority::RevokedSessionError => error
|
|
142
|
+
notify_event("security.token.theft_detected",
|
|
143
|
+
client_id: error.client_id,
|
|
144
|
+
refreshed_session_id: error.refreshed_session_id,
|
|
145
|
+
revoked_session_id: error.revoked_session_id)
|
|
146
|
+
|
|
147
|
+
Rails.logger.warn(error.message)
|
|
148
|
+
render_token_request_error(error: "invalid_request")
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def unsupported_grant_type
|
|
152
|
+
render_token_request_error(error: "unsupported_grant_type")
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def revoke
|
|
156
|
+
notify_event("token.revocation.requested",
|
|
157
|
+
client_id: @token_authority_client&.public_id,
|
|
158
|
+
type_hint: params[:token_type_hint])
|
|
159
|
+
|
|
160
|
+
token = TokenAuthority::JsonWebToken.decode(params[:token])
|
|
161
|
+
token_authority_session = TokenAuthority::Session.find_by(access_token_jti: token[:jti]) ||
|
|
162
|
+
TokenAuthority::Session.find_by(refresh_token_jti: token[:jti])
|
|
163
|
+
|
|
164
|
+
TokenAuthority::Session.revoke_for_token(jti: token[:jti])
|
|
165
|
+
|
|
166
|
+
notify_event("token.revocation.completed",
|
|
167
|
+
client_id: @token_authority_client&.public_id,
|
|
168
|
+
session_id: token_authority_session&.id)
|
|
169
|
+
|
|
170
|
+
head :ok
|
|
171
|
+
rescue JWT::DecodeError
|
|
172
|
+
render_unsupported_token_type_error
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def revoke_access_token
|
|
176
|
+
notify_event("token.revocation.requested",
|
|
177
|
+
client_id: @token_authority_client&.public_id,
|
|
178
|
+
type_hint: "access_token")
|
|
179
|
+
|
|
180
|
+
token = TokenAuthority::AccessToken.from_token(params[:token])
|
|
181
|
+
token_authority_session = TokenAuthority::Session.find_by(access_token_jti: token.jti)
|
|
182
|
+
|
|
183
|
+
TokenAuthority::Session.revoke_for_access_token(access_token_jti: token.jti)
|
|
184
|
+
|
|
185
|
+
notify_event("token.revocation.completed",
|
|
186
|
+
client_id: @token_authority_client&.public_id,
|
|
187
|
+
session_id: token_authority_session&.id)
|
|
188
|
+
|
|
189
|
+
head :ok
|
|
190
|
+
rescue JWT::DecodeError
|
|
191
|
+
render_unsupported_token_type_error
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def revoke_refresh_token
|
|
195
|
+
notify_event("token.revocation.requested",
|
|
196
|
+
client_id: @token_authority_client&.public_id,
|
|
197
|
+
type_hint: "refresh_token")
|
|
198
|
+
|
|
199
|
+
token = TokenAuthority::RefreshToken.from_token(params[:token])
|
|
200
|
+
token_authority_session = TokenAuthority::Session.find_by(refresh_token_jti: token.jti)
|
|
201
|
+
|
|
202
|
+
TokenAuthority::Session.revoke_for_refresh_token(refresh_token_jti: token.jti)
|
|
203
|
+
|
|
204
|
+
notify_event("token.revocation.completed",
|
|
205
|
+
client_id: @token_authority_client&.public_id,
|
|
206
|
+
session_id: token_authority_session&.id)
|
|
207
|
+
|
|
208
|
+
head :ok
|
|
209
|
+
rescue JWT::DecodeError
|
|
210
|
+
render_unsupported_token_type_error
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
private
|
|
214
|
+
|
|
215
|
+
def set_authorization_grant
|
|
216
|
+
@authorization_grant = TokenAuthority::AuthorizationGrant.find_by(public_id: params[:code])
|
|
217
|
+
raise TokenAuthority::InvalidGrantError if @authorization_grant.blank? || @authorization_grant.redeemed?
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def render_token_request_error(error:, status: :bad_request)
|
|
221
|
+
render json: {error:}, status:
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def render_unsupported_token_type_error
|
|
225
|
+
render json: {error: "unsupported_token_type"}, status: :bad_request
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TokenAuthority
|
|
4
|
+
module AuthorizationGrantsHelper
|
|
5
|
+
# Returns a human-friendly display name for a resource URI.
|
|
6
|
+
# Looks up the URI in the configured rfc_8707_resources mapping.
|
|
7
|
+
# Falls back to the URI itself if no mapping is configured.
|
|
8
|
+
#
|
|
9
|
+
# @param resource_uri [String] The resource URI
|
|
10
|
+
# @return [String] The display name or the URI if no mapping exists
|
|
11
|
+
def resource_display_name(resource_uri)
|
|
12
|
+
resources = TokenAuthority.config.rfc_8707_resources || {}
|
|
13
|
+
resources[resource_uri] || resource_uri
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Returns a human-friendly display name for a scope.
|
|
17
|
+
# Looks up the scope in the configured scopes mapping.
|
|
18
|
+
# Falls back to the scope itself if no mapping is configured.
|
|
19
|
+
#
|
|
20
|
+
# @param scope [String] The scope string
|
|
21
|
+
# @return [String] The display name or the scope if no mapping exists
|
|
22
|
+
def scope_display_name(scope)
|
|
23
|
+
scopes = TokenAuthority.config.scopes || {}
|
|
24
|
+
scopes[scope] || scope
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|