standard_id 0.6.0 → 0.7.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/app/controllers/standard_id/api/oauth/revocations_controller.rb +48 -0
- data/app/controllers/standard_id/api/sessions_controller.rb +42 -0
- data/app/controllers/standard_id/api/well_known/openid_configuration_controller.rb +42 -0
- data/config/routes/api.rb +4 -0
- data/lib/standard_id/config/schema.rb +1 -0
- data/lib/standard_id/events/definitions.rb +4 -1
- data/lib/standard_id/passwordless/verification_service.rb +17 -0
- data/lib/standard_id/version.rb +1 -1
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f8e7433ee5b0ef4a2da21afc993c2106cac0d69d03af4fb842f5ab2d8cf2e71b
|
|
4
|
+
data.tar.gz: 7a4eb0f649f83157a77a1200d249eb79391de044470caad81c2bbbd2cc0906b1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fd06a2fa4d70147dadaa175cfd371861a0d723ec38b79cf4991c5d7dab0fb29667f08378654ec17a382673575af501b53d7bd84a1dc750766985e58326639abb
|
|
7
|
+
data.tar.gz: 6686d5ced7aad0df56e585b454e614bb1ddb941a97c7843c97907dfa3ad05682e699a610ee25c31242b2dc8347827991c8e5f41b3a226e2a0c3c314d7bd5ff47
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
module StandardId
|
|
2
|
+
module Api
|
|
3
|
+
module Oauth
|
|
4
|
+
class RevocationsController < BaseController
|
|
5
|
+
public_controller
|
|
6
|
+
|
|
7
|
+
skip_before_action :validate_content_type!
|
|
8
|
+
|
|
9
|
+
# POST /oauth/revoke
|
|
10
|
+
# RFC 7009 - OAuth 2.0 Token Revocation
|
|
11
|
+
#
|
|
12
|
+
# Accepts a token and optional token_type_hint parameter.
|
|
13
|
+
# Always responds with 200 OK regardless of whether the token
|
|
14
|
+
# was valid or revocation was successful (per RFC 7009 Section 2.1).
|
|
15
|
+
def create
|
|
16
|
+
token = params[:token]
|
|
17
|
+
head :ok and return if token.blank?
|
|
18
|
+
|
|
19
|
+
payload = StandardId::JwtService.decode(token)
|
|
20
|
+
head :ok and return unless payload&.dig(:sub)
|
|
21
|
+
|
|
22
|
+
account_id = payload[:sub]
|
|
23
|
+
|
|
24
|
+
sessions = StandardId::DeviceSession
|
|
25
|
+
.where(account_id: account_id)
|
|
26
|
+
.active
|
|
27
|
+
|
|
28
|
+
# token_type_hint is accepted but ignored — we always attempt
|
|
29
|
+
# revocation via sub claim regardless of token type (RFC 7009 §2.1)
|
|
30
|
+
revoked_sessions = sessions.to_a
|
|
31
|
+
if revoked_sessions.any?
|
|
32
|
+
ActiveRecord::Base.transaction do
|
|
33
|
+
revoked_sessions.each { |session| session.revoke!(reason: "token_revocation") }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
StandardId::Events.publish(
|
|
37
|
+
StandardId::Events::OAUTH_TOKEN_REVOKED,
|
|
38
|
+
account_id: account_id,
|
|
39
|
+
sessions_revoked: revoked_sessions.size
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
head :ok
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
module StandardId
|
|
2
|
+
module Api
|
|
3
|
+
class SessionsController < BaseController
|
|
4
|
+
authenticated_controller
|
|
5
|
+
|
|
6
|
+
skip_before_action :validate_content_type!
|
|
7
|
+
before_action :verify_access_token!
|
|
8
|
+
|
|
9
|
+
def index
|
|
10
|
+
sessions = current_account.sessions.active.order(created_at: :desc)
|
|
11
|
+
|
|
12
|
+
render json: sessions.map { |session| serialize_session(session) }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def destroy
|
|
16
|
+
session = current_account.sessions.find_by(id: params[:id])
|
|
17
|
+
|
|
18
|
+
unless session
|
|
19
|
+
render json: { error: "not_found", error_description: "Session not found" }, status: :not_found
|
|
20
|
+
return
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
session.revoke!(reason: "api_revocation")
|
|
24
|
+
head :no_content
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def serialize_session(session)
|
|
30
|
+
{
|
|
31
|
+
id: session.id,
|
|
32
|
+
type: session.type&.demodulize,
|
|
33
|
+
created_at: session.created_at.iso8601,
|
|
34
|
+
last_refreshed_at: session.respond_to?(:last_refreshed_at) ? session.last_refreshed_at&.iso8601 : nil,
|
|
35
|
+
ip_address: session.respond_to?(:ip_address) ? session.ip_address : nil,
|
|
36
|
+
# user_agent is the API-facing name for the device_agent model attribute
|
|
37
|
+
user_agent: session.respond_to?(:device_agent) ? session.device_agent : nil
|
|
38
|
+
}.compact
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
module StandardId
|
|
2
|
+
module Api
|
|
3
|
+
module WellKnown
|
|
4
|
+
class OpenidConfigurationController < ActionController::API
|
|
5
|
+
include StandardId::ControllerPolicy
|
|
6
|
+
public_controller
|
|
7
|
+
|
|
8
|
+
def show
|
|
9
|
+
issuer = StandardId.config.issuer
|
|
10
|
+
|
|
11
|
+
unless issuer.present?
|
|
12
|
+
render json: { error: "Issuer not configured" }, status: :not_found
|
|
13
|
+
return
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
response.headers["Cache-Control"] = "public, max-age=3600"
|
|
17
|
+
render json: discovery_document(issuer)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def discovery_document(issuer)
|
|
23
|
+
base = issuer.chomp("/")
|
|
24
|
+
|
|
25
|
+
{
|
|
26
|
+
issuer: issuer,
|
|
27
|
+
authorization_endpoint: "#{base}/authorize",
|
|
28
|
+
token_endpoint: "#{base}/oauth/token",
|
|
29
|
+
revocation_endpoint: "#{base}/oauth/revoke",
|
|
30
|
+
userinfo_endpoint: "#{base}/userinfo",
|
|
31
|
+
jwks_uri: "#{base}/.well-known/jwks.json",
|
|
32
|
+
response_types_supported: %w[code],
|
|
33
|
+
grant_types_supported: %w[authorization_code refresh_token client_credentials],
|
|
34
|
+
subject_types_supported: %w[public],
|
|
35
|
+
id_token_signing_alg_values_supported: [StandardId.config.oauth.signing_algorithm.to_s.upcase],
|
|
36
|
+
token_endpoint_auth_methods_supported: %w[client_secret_basic client_secret_post]
|
|
37
|
+
}
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
data/config/routes/api.rb
CHANGED
|
@@ -4,6 +4,8 @@ StandardId::ApiEngine.routes.draw do
|
|
|
4
4
|
|
|
5
5
|
resource :userinfo, only: [:show], controller: :userinfo
|
|
6
6
|
|
|
7
|
+
resources :sessions, only: [:index, :destroy]
|
|
8
|
+
|
|
7
9
|
resource :passwordless, only: [], controller: :passwordless do
|
|
8
10
|
post :start
|
|
9
11
|
end
|
|
@@ -14,6 +16,7 @@ StandardId::ApiEngine.routes.draw do
|
|
|
14
16
|
|
|
15
17
|
namespace :oauth do
|
|
16
18
|
resource :token, only: [:create]
|
|
19
|
+
resource :revoke, only: [:create], controller: :revocations
|
|
17
20
|
|
|
18
21
|
namespace :callback do
|
|
19
22
|
post ":provider", to: "providers#callback", as: :provider
|
|
@@ -22,6 +25,7 @@ StandardId::ApiEngine.routes.draw do
|
|
|
22
25
|
|
|
23
26
|
scope ".well-known", module: :well_known do
|
|
24
27
|
get "jwks.json", to: "jwks#show", as: :jwks
|
|
28
|
+
get "openid-configuration", to: "openid_configuration#show", as: :openid_configuration
|
|
25
29
|
end
|
|
26
30
|
end
|
|
27
31
|
end
|
|
@@ -33,6 +33,7 @@ StandardConfig.schema.draw do
|
|
|
33
33
|
field :code_ttl, type: :integer, default: 600 # 10 minutes in seconds
|
|
34
34
|
field :max_attempts, type: :integer, default: 3
|
|
35
35
|
field :retry_delay, type: :integer, default: 30 # 30 seconds
|
|
36
|
+
field :bypass_code, type: :string, default: nil
|
|
36
37
|
end
|
|
37
38
|
|
|
38
39
|
scope :password do
|
|
@@ -39,6 +39,7 @@ module StandardId
|
|
|
39
39
|
OAUTH_TOKEN_ISSUED = "oauth.token.issued"
|
|
40
40
|
OAUTH_TOKEN_REFRESHED = "oauth.token.refreshed"
|
|
41
41
|
OAUTH_CODE_CONSUMED = "oauth.code.consumed"
|
|
42
|
+
OAUTH_TOKEN_REVOKED = "oauth.token.revoked"
|
|
42
43
|
|
|
43
44
|
PASSWORDLESS_CODE_REQUESTED = "passwordless.code.requested"
|
|
44
45
|
PASSWORDLESS_CODE_GENERATED = "passwordless.code.generated"
|
|
@@ -108,7 +109,8 @@ module StandardId
|
|
|
108
109
|
OAUTH_TOKEN_ISSUING,
|
|
109
110
|
OAUTH_TOKEN_ISSUED,
|
|
110
111
|
OAUTH_TOKEN_REFRESHED,
|
|
111
|
-
OAUTH_CODE_CONSUMED
|
|
112
|
+
OAUTH_CODE_CONSUMED,
|
|
113
|
+
OAUTH_TOKEN_REVOKED
|
|
112
114
|
].freeze
|
|
113
115
|
|
|
114
116
|
PASSWORDLESS_EVENTS = [
|
|
@@ -164,6 +166,7 @@ module StandardId
|
|
|
164
166
|
OAUTH_AUTHORIZATION_DENIED,
|
|
165
167
|
OAUTH_TOKEN_ISSUED,
|
|
166
168
|
OAUTH_TOKEN_REFRESHED,
|
|
169
|
+
OAUTH_TOKEN_REVOKED,
|
|
167
170
|
# Passwordless
|
|
168
171
|
PASSWORDLESS_CODE_FAILED,
|
|
169
172
|
PASSWORDLESS_ACCOUNT_CREATED,
|
|
@@ -83,6 +83,9 @@ module StandardId
|
|
|
83
83
|
return failure("Code is required")
|
|
84
84
|
end
|
|
85
85
|
|
|
86
|
+
bypass_result = try_bypass
|
|
87
|
+
return bypass_result if bypass_result
|
|
88
|
+
|
|
86
89
|
challenge = find_active_challenge
|
|
87
90
|
code_matches = challenge.present? && secure_compare(challenge.code, @code)
|
|
88
91
|
attempts = record_failed_attempt(challenge, code_matches)
|
|
@@ -127,6 +130,20 @@ module StandardId
|
|
|
127
130
|
|
|
128
131
|
private
|
|
129
132
|
|
|
133
|
+
# When a bypass_code is configured and the submitted code matches,
|
|
134
|
+
# skip the CodeChallenge lookup entirely. This allows E2E testing
|
|
135
|
+
# tools (e.g. Playwright) to verify OTPs without a real challenge.
|
|
136
|
+
def try_bypass
|
|
137
|
+
bypass_code = StandardId.config.passwordless.bypass_code
|
|
138
|
+
return unless bypass_code.present?
|
|
139
|
+
return unless secure_compare(bypass_code, @code)
|
|
140
|
+
|
|
141
|
+
strategy = strategy_for(@channel)
|
|
142
|
+
account = strategy.find_or_create_account(@target)
|
|
143
|
+
|
|
144
|
+
success(account: account, challenge: nil)
|
|
145
|
+
end
|
|
146
|
+
|
|
130
147
|
def resolve_target_and_channel!(email, phone)
|
|
131
148
|
if email.present?
|
|
132
149
|
@target = email.to_s.strip
|
data/lib/standard_id/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: standard_id
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.7.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jaryl Sim
|
|
@@ -122,11 +122,14 @@ files:
|
|
|
122
122
|
- app/controllers/standard_id/api/base_controller.rb
|
|
123
123
|
- app/controllers/standard_id/api/oauth/base_controller.rb
|
|
124
124
|
- app/controllers/standard_id/api/oauth/callback/providers_controller.rb
|
|
125
|
+
- app/controllers/standard_id/api/oauth/revocations_controller.rb
|
|
125
126
|
- app/controllers/standard_id/api/oauth/tokens_controller.rb
|
|
126
127
|
- app/controllers/standard_id/api/oidc/logout_controller.rb
|
|
127
128
|
- app/controllers/standard_id/api/passwordless_controller.rb
|
|
129
|
+
- app/controllers/standard_id/api/sessions_controller.rb
|
|
128
130
|
- app/controllers/standard_id/api/userinfo_controller.rb
|
|
129
131
|
- app/controllers/standard_id/api/well_known/jwks_controller.rb
|
|
132
|
+
- app/controllers/standard_id/api/well_known/openid_configuration_controller.rb
|
|
130
133
|
- app/controllers/standard_id/web/account_controller.rb
|
|
131
134
|
- app/controllers/standard_id/web/auth/callback/providers_controller.rb
|
|
132
135
|
- app/controllers/standard_id/web/base_controller.rb
|