standard_id-provider 0.1.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 +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +28 -0
- data/Rakefile +6 -0
- data/app/assets/stylesheets/standard_id/provider/application.css +15 -0
- data/app/controllers/standard_id/provider/application_controller.rb +40 -0
- data/app/controllers/standard_id/provider/consent_controller.rb +91 -0
- data/app/controllers/standard_id/provider/discovery_controller.rb +34 -0
- data/app/controllers/standard_id/provider/introspection_controller.rb +41 -0
- data/app/controllers/standard_id/provider/revocation_controller.rb +29 -0
- data/app/helpers/standard_id/provider/application_helper.rb +6 -0
- data/app/jobs/standard_id/provider/application_job.rb +6 -0
- data/app/mailers/standard_id/provider/application_mailer.rb +8 -0
- data/app/models/standard_id/provider/application_record.rb +7 -0
- data/app/models/standard_id/provider/consent_grant.rb +41 -0
- data/app/models/standard_id/provider/revoked_token.rb +31 -0
- data/app/views/layouts/standard_id/provider/application.html.erb +17 -0
- data/config/routes.rb +11 -0
- data/db/migrate/20260312000001_create_standard_id_consent_grants.rb +19 -0
- data/db/migrate/20260312000002_create_standard_id_revoked_tokens.rb +16 -0
- data/lib/standard_id/provider/config/schema.rb +10 -0
- data/lib/standard_id/provider/engine.rb +25 -0
- data/lib/standard_id/provider/extensions/authorization_code_flow_ext.rb +13 -0
- data/lib/standard_id/provider/extensions/authorization_flow_ext.rb +60 -0
- data/lib/standard_id/provider/extensions/token_grant_flow_ext.rb +38 -0
- data/lib/standard_id/provider/extensions/traditional_code_grant_ext.rb +24 -0
- data/lib/standard_id/provider/id_token_service.rb +40 -0
- data/lib/standard_id/provider/version.rb +5 -0
- data/lib/standard_id/provider.rb +15 -0
- data/lib/tasks/standard_id/provider_tasks.rake +4 -0
- metadata +102 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 7277bed83877f6bf4d594edaf754208a194edc113b166428005491855f5c4e94
|
|
4
|
+
data.tar.gz: 6bb65c12882616a7b5116e56d82b6aae805ffcce9e021621f2572a00f4c4f4a3
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: f1648a8f251196809c60e44ab87dc8611e943f73c27e5430b70660c65af010eb523ac8dcce958ec992786e3784e571a67b49dc057ed1475079d8fd635d5410c1
|
|
7
|
+
data.tar.gz: 5ee6aaf6993590ddbf1c2139c26d11c079d0b3aee394cf38c48fe05b5cebaf85e36bbbefed4b8e641e9cfc63d657b66349e0442904b08db3ab95519d9566ac89
|
data/MIT-LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Copyright (c) Jaryl Sim
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
4
|
+
a copy of this software and associated documentation files (the
|
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
9
|
+
the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be
|
|
12
|
+
included in all copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# StandardId::Provider
|
|
2
|
+
Short description and motivation.
|
|
3
|
+
|
|
4
|
+
## Usage
|
|
5
|
+
How to use my plugin.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
Add this line to your application's Gemfile:
|
|
9
|
+
|
|
10
|
+
```ruby
|
|
11
|
+
gem "standard_id-provider"
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
And then execute:
|
|
15
|
+
```bash
|
|
16
|
+
$ bundle
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Or install it yourself as:
|
|
20
|
+
```bash
|
|
21
|
+
$ gem install standard_id-provider
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Contributing
|
|
25
|
+
Contribution directions go here.
|
|
26
|
+
|
|
27
|
+
## License
|
|
28
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This is a manifest file that'll be compiled into application.css, which will include all the files
|
|
3
|
+
* listed below.
|
|
4
|
+
*
|
|
5
|
+
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
|
|
6
|
+
* or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
|
|
7
|
+
*
|
|
8
|
+
* You're free to add application-wide styles to this file and they'll appear at the bottom of the
|
|
9
|
+
* compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
|
|
10
|
+
* files in this directory. Styles in this file should be added after the last require_* statement.
|
|
11
|
+
* It is generally better to create a new file per style scope.
|
|
12
|
+
*
|
|
13
|
+
*= require_tree .
|
|
14
|
+
*= require_self
|
|
15
|
+
*/
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
module StandardId
|
|
2
|
+
module Provider
|
|
3
|
+
class ApplicationController < ActionController::API
|
|
4
|
+
rescue_from StandardId::OAuthError do |e|
|
|
5
|
+
render json: { error: e.oauth_error_code, error_description: e.message }, status: e.http_status
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
def authenticate_client!
|
|
11
|
+
extract_client_credentials_from_basic_auth
|
|
12
|
+
|
|
13
|
+
@client_credential = StandardId::ClientSecretCredential.active.find_by(client_id: params[:client_id])
|
|
14
|
+
|
|
15
|
+
unless @client_credential&.authenticate_client_secret(params[:client_secret])
|
|
16
|
+
raise StandardId::InvalidClientError, "Client authentication failed"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
@client_credential
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def extract_client_credentials_from_basic_auth
|
|
23
|
+
auth_header = request.headers["Authorization"]
|
|
24
|
+
return unless auth_header&.start_with?("Basic ")
|
|
25
|
+
|
|
26
|
+
if params[:client_id].present? || params[:client_secret].present?
|
|
27
|
+
raise StandardId::InvalidRequestError,
|
|
28
|
+
"Client credentials must be sent via Authorization header OR request body, not both"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
decoded = Base64.strict_decode64(auth_header.split(" ", 2).last)
|
|
32
|
+
client_id, client_secret = decoded.split(":", 2)
|
|
33
|
+
params[:client_id] = CGI.unescape(client_id)
|
|
34
|
+
params[:client_secret] = CGI.unescape(client_secret)
|
|
35
|
+
rescue ArgumentError
|
|
36
|
+
raise StandardId::InvalidClientError, "Invalid Basic authentication encoding"
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
module StandardId
|
|
2
|
+
module Provider
|
|
3
|
+
class ConsentController < ApplicationController
|
|
4
|
+
include ActionController::Cookies
|
|
5
|
+
|
|
6
|
+
before_action :require_authentication
|
|
7
|
+
before_action :load_client
|
|
8
|
+
|
|
9
|
+
def show
|
|
10
|
+
render json: {
|
|
11
|
+
client: { name: @client.name, description: @client.description },
|
|
12
|
+
scopes: requested_scopes,
|
|
13
|
+
authorization_params: authorization_params
|
|
14
|
+
}
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def create
|
|
18
|
+
ConsentGrant.grant!(
|
|
19
|
+
account: current_account,
|
|
20
|
+
client_application: @client,
|
|
21
|
+
scopes: authorization_params[:scope]
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
authorize_url = build_authorize_redirect
|
|
25
|
+
redirect_to authorize_url, allow_other_host: true, status: :found
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def destroy
|
|
29
|
+
redirect_uri = authorization_params[:redirect_uri]
|
|
30
|
+
|
|
31
|
+
if redirect_uri.present?
|
|
32
|
+
deny_url = build_error_redirect(redirect_uri, "access_denied", "The user denied the consent request")
|
|
33
|
+
redirect_to deny_url, allow_other_host: true, status: :found
|
|
34
|
+
else
|
|
35
|
+
render json: { error: "access_denied", error_description: "The user denied the consent request" }, status: :forbidden
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def require_authentication
|
|
42
|
+
return if current_account.present?
|
|
43
|
+
|
|
44
|
+
render json: { error: "login_required", error_description: "Authentication is required" }, status: :unauthorized
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def current_account
|
|
48
|
+
@current_account ||= begin
|
|
49
|
+
token_manager = StandardId::Web::TokenManager.new(request)
|
|
50
|
+
session_manager = StandardId::Web::SessionManager.new(token_manager, request: request, session: session, cookies: cookies)
|
|
51
|
+
session_manager.current_account
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def load_client
|
|
56
|
+
@client = StandardId::ClientApplication.active.find_by(client_id: authorization_params[:client_id])
|
|
57
|
+
|
|
58
|
+
unless @client
|
|
59
|
+
raise StandardId::InvalidClientError, "Invalid client_id"
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def authorization_params
|
|
64
|
+
@authorization_params ||= params.permit(
|
|
65
|
+
:client_id, :redirect_uri, :scope, :state, :audience,
|
|
66
|
+
:nonce, :response_type, :code_challenge, :code_challenge_method
|
|
67
|
+
).to_h.symbolize_keys
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def requested_scopes
|
|
71
|
+
authorization_params[:scope].to_s.split(/\s+/)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def build_authorize_redirect
|
|
75
|
+
uri = URI.parse("#{StandardId.config.issuer}/api/authorize")
|
|
76
|
+
uri.query = URI.encode_www_form(authorization_params.compact)
|
|
77
|
+
uri.to_s
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def build_error_redirect(redirect_uri, error, description)
|
|
81
|
+
uri = URI.parse(redirect_uri)
|
|
82
|
+
query = URI.decode_www_form(uri.query || "")
|
|
83
|
+
query << [ "error", error ]
|
|
84
|
+
query << [ "error_description", description ]
|
|
85
|
+
query << [ "state", authorization_params[:state] ] if authorization_params[:state].present?
|
|
86
|
+
uri.query = URI.encode_www_form(query)
|
|
87
|
+
uri.to_s
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
module StandardId
|
|
2
|
+
module Provider
|
|
3
|
+
class DiscoveryController < ApplicationController
|
|
4
|
+
def show
|
|
5
|
+
render json: openid_configuration
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
def openid_configuration
|
|
11
|
+
issuer = StandardId.config.issuer
|
|
12
|
+
provider_config = StandardId.config.provider
|
|
13
|
+
|
|
14
|
+
{
|
|
15
|
+
issuer: issuer,
|
|
16
|
+
authorization_endpoint: "#{issuer}/api/authorize",
|
|
17
|
+
token_endpoint: "#{issuer}/api/oauth/token",
|
|
18
|
+
userinfo_endpoint: "#{issuer}/api/userinfo",
|
|
19
|
+
jwks_uri: "#{issuer}/api/.well-known/jwks.json",
|
|
20
|
+
introspection_endpoint: "#{issuer}/api/provider/introspect",
|
|
21
|
+
revocation_endpoint: "#{issuer}/api/provider/revoke",
|
|
22
|
+
scopes_supported: provider_config.scopes_supported,
|
|
23
|
+
response_types_supported: %w[code token],
|
|
24
|
+
grant_types_supported: %w[authorization_code client_credentials refresh_token],
|
|
25
|
+
subject_types_supported: provider_config.subject_types_supported,
|
|
26
|
+
id_token_signing_alg_values_supported: [ StandardId::JwtService.algorithm ],
|
|
27
|
+
token_endpoint_auth_methods_supported: %w[client_secret_basic client_secret_post],
|
|
28
|
+
claims_supported: provider_config.claims_supported,
|
|
29
|
+
code_challenge_methods_supported: %w[S256 plain]
|
|
30
|
+
}.compact
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
module StandardId
|
|
2
|
+
module Provider
|
|
3
|
+
class IntrospectionController < ApplicationController
|
|
4
|
+
before_action :authenticate_client!
|
|
5
|
+
|
|
6
|
+
def create
|
|
7
|
+
token = params[:token]
|
|
8
|
+
|
|
9
|
+
if token.blank?
|
|
10
|
+
render json: { active: false }
|
|
11
|
+
return
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
payload = StandardId::JwtService.decode(token)
|
|
15
|
+
|
|
16
|
+
if payload.nil?
|
|
17
|
+
render json: { active: false }
|
|
18
|
+
return
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
if payload[:jti].present? && RevokedToken.revoked?(payload[:jti])
|
|
22
|
+
render json: { active: false }
|
|
23
|
+
return
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
render json: {
|
|
27
|
+
active: true,
|
|
28
|
+
sub: payload[:sub],
|
|
29
|
+
client_id: payload[:client_id],
|
|
30
|
+
scope: payload[:scope],
|
|
31
|
+
iss: payload[:iss],
|
|
32
|
+
exp: payload[:exp],
|
|
33
|
+
iat: payload[:iat],
|
|
34
|
+
jti: payload[:jti],
|
|
35
|
+
aud: payload[:aud],
|
|
36
|
+
token_type: "Bearer"
|
|
37
|
+
}.compact
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module StandardId
|
|
2
|
+
module Provider
|
|
3
|
+
class RevocationController < ApplicationController
|
|
4
|
+
before_action :authenticate_client!
|
|
5
|
+
|
|
6
|
+
def create
|
|
7
|
+
token = params[:token]
|
|
8
|
+
return head :ok if token.blank?
|
|
9
|
+
|
|
10
|
+
payload = StandardId::JwtService.decode(token)
|
|
11
|
+
return head :ok if payload.nil?
|
|
12
|
+
|
|
13
|
+
jti = payload[:jti]
|
|
14
|
+
return head :ok if jti.blank?
|
|
15
|
+
|
|
16
|
+
expires_at = payload[:exp] ? Time.at(payload[:exp]) : 1.day.from_now
|
|
17
|
+
|
|
18
|
+
RevokedToken.revoke!(
|
|
19
|
+
jti: jti,
|
|
20
|
+
client_id: @client_credential.client_id,
|
|
21
|
+
token_type: params[:token_type_hint],
|
|
22
|
+
expires_at: expires_at
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
head :ok
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
module StandardId
|
|
2
|
+
module Provider
|
|
3
|
+
class ConsentGrant < ApplicationRecord
|
|
4
|
+
self.table_name = "standard_id_consent_grants"
|
|
5
|
+
|
|
6
|
+
belongs_to :account, class_name: StandardId.config.account_class_name
|
|
7
|
+
belongs_to :client_application, class_name: "StandardId::ClientApplication"
|
|
8
|
+
|
|
9
|
+
validates :scopes, presence: true
|
|
10
|
+
|
|
11
|
+
scope :active, -> { where(revoked_at: nil) }
|
|
12
|
+
scope :revoked, -> { where.not(revoked_at: nil) }
|
|
13
|
+
|
|
14
|
+
before_validation :set_granted_at, on: :create
|
|
15
|
+
|
|
16
|
+
def self.grant!(account:, client_application:, scopes:)
|
|
17
|
+
transaction do
|
|
18
|
+
active.where(account: account, client_application: client_application).each(&:revoke!)
|
|
19
|
+
create!(account: account, client_application: client_application, scopes: scopes)
|
|
20
|
+
end
|
|
21
|
+
rescue ActiveRecord::RecordNotUnique
|
|
22
|
+
retry
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def revoke!
|
|
26
|
+
update!(revoked_at: Time.current)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def covers_scopes?(requested_scopes)
|
|
30
|
+
granted = scopes.to_s.split(/\s+/)
|
|
31
|
+
(requested_scopes - granted).empty?
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def set_granted_at
|
|
37
|
+
self.granted_at ||= Time.current
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
module StandardId
|
|
2
|
+
module Provider
|
|
3
|
+
class RevokedToken < ApplicationRecord
|
|
4
|
+
self.table_name = "standard_id_revoked_tokens"
|
|
5
|
+
|
|
6
|
+
validates :jti, presence: true, uniqueness: true
|
|
7
|
+
|
|
8
|
+
scope :active, -> { where("expires_at > ?", Time.current) }
|
|
9
|
+
|
|
10
|
+
def self.revoke!(jti:, client_id: nil, token_type: nil, expires_at: nil)
|
|
11
|
+
create!(
|
|
12
|
+
jti: jti,
|
|
13
|
+
client_id: client_id,
|
|
14
|
+
token_type: token_type,
|
|
15
|
+
revoked_at: Time.current,
|
|
16
|
+
expires_at: expires_at || 1.day.from_now
|
|
17
|
+
)
|
|
18
|
+
rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotUnique
|
|
19
|
+
# Token already revoked — idempotent
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.revoked?(jti)
|
|
23
|
+
where(jti: jti).exists?
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.cleanup_expired!
|
|
27
|
+
where("expires_at <= ?", Time.current).delete_all
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<title>Standard id provider</title>
|
|
5
|
+
<%= csrf_meta_tags %>
|
|
6
|
+
<%= csp_meta_tag %>
|
|
7
|
+
|
|
8
|
+
<%= yield :head %>
|
|
9
|
+
|
|
10
|
+
<%= stylesheet_link_tag "standard_id/provider/application", media: "all" %>
|
|
11
|
+
</head>
|
|
12
|
+
<body>
|
|
13
|
+
|
|
14
|
+
<%= yield %>
|
|
15
|
+
|
|
16
|
+
</body>
|
|
17
|
+
</html>
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
StandardId::Provider::Engine.routes.draw do
|
|
2
|
+
scope ".well-known" do
|
|
3
|
+
get "openid-configuration", to: "discovery#show"
|
|
4
|
+
end
|
|
5
|
+
|
|
6
|
+
scope "api/provider" do
|
|
7
|
+
post "introspect", to: "introspection#create"
|
|
8
|
+
post "revoke", to: "revocation#create"
|
|
9
|
+
resource :consent, only: [ :show, :create, :destroy ], controller: :consent
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
class CreateStandardIdConsentGrants < ActiveRecord::Migration[8.0]
|
|
2
|
+
def change
|
|
3
|
+
create_table :standard_id_consent_grants do |t|
|
|
4
|
+
t.references :account, null: false
|
|
5
|
+
t.references :client_application, null: false
|
|
6
|
+
t.string :scopes, null: false
|
|
7
|
+
t.datetime :granted_at, null: false
|
|
8
|
+
t.datetime :revoked_at
|
|
9
|
+
|
|
10
|
+
t.timestamps
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
add_index :standard_id_consent_grants,
|
|
14
|
+
[ :account_id, :client_application_id ],
|
|
15
|
+
unique: true,
|
|
16
|
+
where: "revoked_at IS NULL",
|
|
17
|
+
name: "idx_consent_grants_active_unique"
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
class CreateStandardIdRevokedTokens < ActiveRecord::Migration[8.0]
|
|
2
|
+
def change
|
|
3
|
+
create_table :standard_id_revoked_tokens do |t|
|
|
4
|
+
t.string :jti, null: false
|
|
5
|
+
t.string :token_type
|
|
6
|
+
t.string :client_id
|
|
7
|
+
t.datetime :revoked_at, null: false
|
|
8
|
+
t.datetime :expires_at, null: false
|
|
9
|
+
|
|
10
|
+
t.timestamps
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
add_index :standard_id_revoked_tokens, :jti, unique: true
|
|
14
|
+
add_index :standard_id_revoked_tokens, :expires_at
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
StandardConfig.schema.draw do
|
|
2
|
+
scope :provider do
|
|
3
|
+
field :id_token_lifetime, type: :integer, default: 3600
|
|
4
|
+
field :scopes_supported, type: :array, default: -> { %w[openid profile email offline_access] }
|
|
5
|
+
field :claims_supported, type: :array, default: -> { %w[sub iss aud exp iat nonce auth_time at_hash email name email_verified] }
|
|
6
|
+
field :subject_types_supported, type: :array, default: -> { %w[public] }
|
|
7
|
+
field :introspection_enabled, type: :boolean, default: true
|
|
8
|
+
field :revocation_enabled, type: :boolean, default: true
|
|
9
|
+
end
|
|
10
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module StandardId
|
|
2
|
+
module Provider
|
|
3
|
+
class Engine < ::Rails::Engine
|
|
4
|
+
isolate_namespace StandardId::Provider
|
|
5
|
+
|
|
6
|
+
initializer "standard_id_provider.extend_core" do
|
|
7
|
+
StandardId::Oauth::TokenGrantFlow.prepend(
|
|
8
|
+
StandardId::Provider::Extensions::TokenGrantFlowExt
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
StandardId::Oauth::AuthorizationCodeAuthorizationFlow.prepend(
|
|
12
|
+
StandardId::Provider::Extensions::AuthorizationFlowExt
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
StandardId::Oauth::Subflows::TraditionalCodeGrant.prepend(
|
|
16
|
+
StandardId::Provider::Extensions::TraditionalCodeGrantExt
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
StandardId::Oauth::AuthorizationCodeFlow.prepend(
|
|
20
|
+
StandardId::Provider::Extensions::AuthorizationCodeFlowExt
|
|
21
|
+
)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
module StandardId
|
|
2
|
+
module Provider
|
|
3
|
+
module Extensions
|
|
4
|
+
module AuthorizationFlowExt
|
|
5
|
+
private
|
|
6
|
+
|
|
7
|
+
def generate_authorization_response
|
|
8
|
+
if consent_required? && !consent_granted?
|
|
9
|
+
return redirect_to_consent
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
super
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def common_subflow_params(flow_params)
|
|
16
|
+
super.merge(nonce: flow_params[:nonce])
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def consent_required?
|
|
20
|
+
@client&.require_consent? && openid_scope_requested?
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def openid_scope_requested?
|
|
24
|
+
scope.to_s.split(/\s+/).include?("openid")
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def consent_granted?
|
|
28
|
+
return false unless current_account
|
|
29
|
+
|
|
30
|
+
grant = StandardId::Provider::ConsentGrant.active
|
|
31
|
+
.find_by(account: current_account, client_application: @client)
|
|
32
|
+
|
|
33
|
+
return false unless grant
|
|
34
|
+
|
|
35
|
+
requested = scope.to_s.split(/\s+/).sort
|
|
36
|
+
granted = grant.scopes.to_s.split(/\s+/).sort
|
|
37
|
+
(requested - granted).empty?
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def redirect_to_consent
|
|
41
|
+
consent_params = {
|
|
42
|
+
client_id: params[:client_id],
|
|
43
|
+
redirect_uri: redirect_uri,
|
|
44
|
+
scope: scope,
|
|
45
|
+
state: state,
|
|
46
|
+
audience: audience,
|
|
47
|
+
nonce: params[:nonce],
|
|
48
|
+
response_type: params[:response_type],
|
|
49
|
+
code_challenge: params[:code_challenge],
|
|
50
|
+
code_challenge_method: params[:code_challenge_method]
|
|
51
|
+
}.compact
|
|
52
|
+
|
|
53
|
+
consent_url = StandardId::Provider::Engine.routes.url_helpers.consent_path(consent_params)
|
|
54
|
+
|
|
55
|
+
{ redirect_to: consent_url, status: :found }
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
module StandardId
|
|
2
|
+
module Provider
|
|
3
|
+
module Extensions
|
|
4
|
+
module TokenGrantFlowExt
|
|
5
|
+
private
|
|
6
|
+
|
|
7
|
+
def generate_token_response
|
|
8
|
+
response = super
|
|
9
|
+
|
|
10
|
+
if openid_scope_requested? && subject_id.present?
|
|
11
|
+
response[:id_token] = Provider::IdTokenService.generate(
|
|
12
|
+
subject_id: subject_id,
|
|
13
|
+
client_id: client_id,
|
|
14
|
+
nonce: id_token_nonce,
|
|
15
|
+
access_token: response[:access_token],
|
|
16
|
+
auth_time: id_token_auth_time
|
|
17
|
+
)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
response
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def build_jwt_payload(expires_in)
|
|
24
|
+
payload = super
|
|
25
|
+
payload[:jti] = SecureRandom.uuid
|
|
26
|
+
payload
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def openid_scope_requested?
|
|
30
|
+
current_scopes.include?("openid")
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def id_token_nonce = nil
|
|
34
|
+
def id_token_auth_time = nil
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
module StandardId
|
|
2
|
+
module Provider
|
|
3
|
+
module Extensions
|
|
4
|
+
module TraditionalCodeGrantExt
|
|
5
|
+
private
|
|
6
|
+
|
|
7
|
+
def store_authorization_code
|
|
8
|
+
StandardId::AuthorizationCode.issue!(
|
|
9
|
+
plaintext_code: authorization_code,
|
|
10
|
+
client_id: params[:client_id],
|
|
11
|
+
redirect_uri: params[:redirect_uri],
|
|
12
|
+
scope: params[:scope],
|
|
13
|
+
audience: params[:audience],
|
|
14
|
+
account: params[:current_account],
|
|
15
|
+
code_challenge: params[:code_challenge],
|
|
16
|
+
code_challenge_method: params[:code_challenge_method],
|
|
17
|
+
nonce: params[:nonce],
|
|
18
|
+
metadata: { state: params[:state] }.compact
|
|
19
|
+
)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
module StandardId
|
|
2
|
+
module Provider
|
|
3
|
+
class IdTokenService
|
|
4
|
+
class << self
|
|
5
|
+
def generate(subject_id:, client_id:, nonce: nil, access_token: nil, authorization_code: nil, auth_time: nil, extra_claims: {})
|
|
6
|
+
payload = {
|
|
7
|
+
sub: subject_id,
|
|
8
|
+
aud: client_id,
|
|
9
|
+
nonce: nonce,
|
|
10
|
+
auth_time: auth_time&.to_i
|
|
11
|
+
}.compact
|
|
12
|
+
|
|
13
|
+
payload[:at_hash] = compute_half_hash(access_token) if access_token.present?
|
|
14
|
+
payload[:c_hash] = compute_half_hash(authorization_code) if authorization_code.present?
|
|
15
|
+
payload.merge!(extra_claims.symbolize_keys) if extra_claims.present?
|
|
16
|
+
|
|
17
|
+
expires_in = StandardId.config.provider.id_token_lifetime.seconds
|
|
18
|
+
|
|
19
|
+
StandardId::JwtService.encode(payload, expires_in: expires_in)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def compute_half_hash(value)
|
|
25
|
+
digest = hash_algorithm.digest(value)
|
|
26
|
+
Base64.urlsafe_encode64(digest[0, digest.length / 2], padding: false)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def hash_algorithm
|
|
30
|
+
case StandardId::JwtService.algorithm
|
|
31
|
+
when /256$/ then OpenSSL::Digest::SHA256
|
|
32
|
+
when /384$/ then OpenSSL::Digest::SHA384
|
|
33
|
+
when /512$/ then OpenSSL::Digest::SHA512
|
|
34
|
+
else OpenSSL::Digest::SHA256
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
require "standard_id"
|
|
2
|
+
|
|
3
|
+
require "standard_id/provider/version"
|
|
4
|
+
require "standard_id/provider/config/schema"
|
|
5
|
+
require "standard_id/provider/id_token_service"
|
|
6
|
+
require "standard_id/provider/extensions/token_grant_flow_ext"
|
|
7
|
+
require "standard_id/provider/extensions/authorization_flow_ext"
|
|
8
|
+
require "standard_id/provider/extensions/traditional_code_grant_ext"
|
|
9
|
+
require "standard_id/provider/extensions/authorization_code_flow_ext"
|
|
10
|
+
require "standard_id/provider/engine"
|
|
11
|
+
|
|
12
|
+
module StandardId
|
|
13
|
+
module Provider
|
|
14
|
+
end
|
|
15
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: standard_id-provider
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Jaryl Sim
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: rails
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '8.0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '8.0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: standard_id
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '0.3'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0.3'
|
|
40
|
+
description: 'Extends StandardId with full OIDC Identity Provider capabilities: ID
|
|
41
|
+
tokens, consent management, token introspection, token revocation, and discovery.'
|
|
42
|
+
email:
|
|
43
|
+
- code@jaryl.dev
|
|
44
|
+
executables: []
|
|
45
|
+
extensions: []
|
|
46
|
+
extra_rdoc_files: []
|
|
47
|
+
files:
|
|
48
|
+
- MIT-LICENSE
|
|
49
|
+
- README.md
|
|
50
|
+
- Rakefile
|
|
51
|
+
- app/assets/stylesheets/standard_id/provider/application.css
|
|
52
|
+
- app/controllers/standard_id/provider/application_controller.rb
|
|
53
|
+
- app/controllers/standard_id/provider/consent_controller.rb
|
|
54
|
+
- app/controllers/standard_id/provider/discovery_controller.rb
|
|
55
|
+
- app/controllers/standard_id/provider/introspection_controller.rb
|
|
56
|
+
- app/controllers/standard_id/provider/revocation_controller.rb
|
|
57
|
+
- app/helpers/standard_id/provider/application_helper.rb
|
|
58
|
+
- app/jobs/standard_id/provider/application_job.rb
|
|
59
|
+
- app/mailers/standard_id/provider/application_mailer.rb
|
|
60
|
+
- app/models/standard_id/provider/application_record.rb
|
|
61
|
+
- app/models/standard_id/provider/consent_grant.rb
|
|
62
|
+
- app/models/standard_id/provider/revoked_token.rb
|
|
63
|
+
- app/views/layouts/standard_id/provider/application.html.erb
|
|
64
|
+
- config/routes.rb
|
|
65
|
+
- db/migrate/20260312000001_create_standard_id_consent_grants.rb
|
|
66
|
+
- db/migrate/20260312000002_create_standard_id_revoked_tokens.rb
|
|
67
|
+
- lib/standard_id/provider.rb
|
|
68
|
+
- lib/standard_id/provider/config/schema.rb
|
|
69
|
+
- lib/standard_id/provider/engine.rb
|
|
70
|
+
- lib/standard_id/provider/extensions/authorization_code_flow_ext.rb
|
|
71
|
+
- lib/standard_id/provider/extensions/authorization_flow_ext.rb
|
|
72
|
+
- lib/standard_id/provider/extensions/token_grant_flow_ext.rb
|
|
73
|
+
- lib/standard_id/provider/extensions/traditional_code_grant_ext.rb
|
|
74
|
+
- lib/standard_id/provider/id_token_service.rb
|
|
75
|
+
- lib/standard_id/provider/version.rb
|
|
76
|
+
- lib/tasks/standard_id/provider_tasks.rake
|
|
77
|
+
homepage: https://github.com/rarebit-one/standard_id-provider
|
|
78
|
+
licenses:
|
|
79
|
+
- MIT
|
|
80
|
+
metadata:
|
|
81
|
+
homepage_uri: https://github.com/rarebit-one/standard_id-provider
|
|
82
|
+
source_code_uri: https://github.com/rarebit-one/standard_id-provider
|
|
83
|
+
changelog_uri: https://github.com/rarebit-one/standard_id-provider/blob/main/CHANGELOG.md
|
|
84
|
+
bug_tracker_uri: https://github.com/rarebit-one/standard_id-provider/issues
|
|
85
|
+
rdoc_options: []
|
|
86
|
+
require_paths:
|
|
87
|
+
- lib
|
|
88
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
89
|
+
requirements:
|
|
90
|
+
- - ">="
|
|
91
|
+
- !ruby/object:Gem::Version
|
|
92
|
+
version: '3.2'
|
|
93
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
94
|
+
requirements:
|
|
95
|
+
- - ">="
|
|
96
|
+
- !ruby/object:Gem::Version
|
|
97
|
+
version: '0'
|
|
98
|
+
requirements: []
|
|
99
|
+
rubygems_version: 4.0.3
|
|
100
|
+
specification_version: 4
|
|
101
|
+
summary: OpenID Connect Identity Provider addon for StandardId.
|
|
102
|
+
test_files: []
|