himari 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/.rspec +3 -0
- data/Gemfile +11 -0
- data/Gemfile.lock +152 -0
- data/LICENSE.txt +21 -0
- data/Rakefile +8 -0
- data/himari.gemspec +44 -0
- data/lib/himari/access_token.rb +119 -0
- data/lib/himari/app.rb +193 -0
- data/lib/himari/authorization_code.rb +83 -0
- data/lib/himari/client_registration.rb +47 -0
- data/lib/himari/config.rb +39 -0
- data/lib/himari/decisions/authentication.rb +16 -0
- data/lib/himari/decisions/authorization.rb +48 -0
- data/lib/himari/decisions/base.rb +63 -0
- data/lib/himari/decisions/claims.rb +58 -0
- data/lib/himari/id_token.rb +57 -0
- data/lib/himari/item_provider.rb +11 -0
- data/lib/himari/item_providers/static.rb +20 -0
- data/lib/himari/log_line.rb +9 -0
- data/lib/himari/middlewares/authentication_rule.rb +24 -0
- data/lib/himari/middlewares/authorization_rule.rb +24 -0
- data/lib/himari/middlewares/claims_rule.rb +24 -0
- data/lib/himari/middlewares/client.rb +24 -0
- data/lib/himari/middlewares/config.rb +24 -0
- data/lib/himari/middlewares/signing_key.rb +24 -0
- data/lib/himari/provider_chain.rb +26 -0
- data/lib/himari/rule.rb +7 -0
- data/lib/himari/rule_processor.rb +81 -0
- data/lib/himari/services/downstream_authorization.rb +73 -0
- data/lib/himari/services/jwks_endpoint.rb +40 -0
- data/lib/himari/services/oidc_authorization_endpoint.rb +82 -0
- data/lib/himari/services/oidc_provider_metadata_endpoint.rb +56 -0
- data/lib/himari/services/oidc_token_endpoint.rb +86 -0
- data/lib/himari/services/oidc_userinfo_endpoint.rb +73 -0
- data/lib/himari/services/upstream_authentication.rb +106 -0
- data/lib/himari/session_data.rb +7 -0
- data/lib/himari/signing_key.rb +128 -0
- data/lib/himari/storages/base.rb +57 -0
- data/lib/himari/storages/filesystem.rb +36 -0
- data/lib/himari/storages/memory.rb +31 -0
- data/lib/himari/version.rb +5 -0
- data/lib/himari.rb +4 -0
- data/public/public/index.css +74 -0
- data/sig/himari.rbs +4 -0
- data/views/login.erb +37 -0
- metadata +174 -0
@@ -0,0 +1,40 @@
|
|
1
|
+
module Himari
|
2
|
+
module Services
|
3
|
+
class JwksEndpoint
|
4
|
+
# @param signing_key_provider [Himari::ProviderChain<Himari::SigningKey>]
|
5
|
+
def initialize(signing_key_provider:)
|
6
|
+
@signing_key_provider = signing_key_provider
|
7
|
+
end
|
8
|
+
|
9
|
+
def app
|
10
|
+
self
|
11
|
+
end
|
12
|
+
|
13
|
+
def call(env)
|
14
|
+
Handler.new(signing_key_provider: @signing_key_provider, env: env).response
|
15
|
+
end
|
16
|
+
|
17
|
+
class Handler
|
18
|
+
class InvalidToken < StandardError; end
|
19
|
+
|
20
|
+
def initialize(signing_key_provider:, env:)
|
21
|
+
@signing_key_provider = signing_key_provider
|
22
|
+
@env = env
|
23
|
+
end
|
24
|
+
|
25
|
+
def response
|
26
|
+
# https://www.rfc-editor.org/rfc/rfc7517#section-5
|
27
|
+
return [404, {'Content-Type' => 'application/json'}, ['{"error": "not_found"}']] unless @env['REQUEST_METHOD'] == 'GET'
|
28
|
+
|
29
|
+
signing_keys = @signing_key_provider.collect()
|
30
|
+
|
31
|
+
[
|
32
|
+
200,
|
33
|
+
{'Content-Type' => 'application/json; charset=utf-8'},
|
34
|
+
[JSON.pretty_generate(keys: signing_keys.map(&:as_jwk)), "\n"],
|
35
|
+
]
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
require 'rack/oauth2'
|
2
|
+
require 'digest/sha2'
|
3
|
+
require 'openid_connect'
|
4
|
+
|
5
|
+
module Himari
|
6
|
+
module Services
|
7
|
+
class OidcAuthorizationEndpoint
|
8
|
+
SUPPORTED_RESPONSE_TYPES = ['code'] # TODO: share with oidc metadata
|
9
|
+
|
10
|
+
# @param authz [Himari::AuthorizationCode] pending (unpersisted) authz data
|
11
|
+
# @param client [Himari::ClientRegistration]
|
12
|
+
# @param storage [Himari::Storages::Base]
|
13
|
+
# @param logger [Logger]
|
14
|
+
def initialize(authz:, client:, storage:, logger: nil)
|
15
|
+
@authz = authz
|
16
|
+
@client = client
|
17
|
+
@storage = storage
|
18
|
+
@logger = logger
|
19
|
+
end
|
20
|
+
|
21
|
+
def call(env)
|
22
|
+
app(env).call(env)
|
23
|
+
rescue Rack::OAuth2::Server::Abstract::Error => e
|
24
|
+
@logger&.warn(Himari::LogLine.new('OidcAuthorizationEndpoint: returning error', req: env['himari.request_as_log'], err: e.class.inspect, err_content: e.protocol_params))
|
25
|
+
# XXX: finish???? https://github.com/nov/rack-oauth2/blob/v2.2.0/lib/rack/oauth2/server/authorize/error.rb#L19
|
26
|
+
# Call https://github.com/nov/rack-oauth2/blob/v2.2.0/lib/rack/oauth2/server/abstract/error.rb#L25
|
27
|
+
Rack::OAuth2::Server::Abstract::Error.instance_method(:finish).bind(e).call
|
28
|
+
end
|
29
|
+
|
30
|
+
def app(env)
|
31
|
+
Rack::OAuth2::Server::Authorize.new do |req, res|
|
32
|
+
# sanity check
|
33
|
+
unless @client.id == req.client_id
|
34
|
+
@logger&.warn(Himari::LogLine.new('OidcAuthorizationEndpoint: @client.id != req.client_id', req: env['himari.request_as_log'], known_client: @client.id, given_client: req.client_id))
|
35
|
+
next req.bad_request!
|
36
|
+
end
|
37
|
+
raise "[BUG] client.id != authz.cilent_id" unless @authz.client_id == @client.id
|
38
|
+
res.redirect_uri = req.verify_redirect_uri!(@client.redirect_uris)
|
39
|
+
|
40
|
+
req.unsupported_response_type! if res.protocol_params_location == :fragment
|
41
|
+
req.bad_request!(:request_uri_not_supported, "Request Object is not implemented") if req.request_uri || req.request
|
42
|
+
|
43
|
+
requested_response_types = [*req.response_type]
|
44
|
+
unless SUPPORTED_RESPONSE_TYPES.include?(requested_response_types.map(&:to_s).join(' '))
|
45
|
+
next req.unsupported_response_type!
|
46
|
+
end
|
47
|
+
|
48
|
+
if requested_response_types.include?(:code)
|
49
|
+
@authz.redirect_uri = res.redirect_uri
|
50
|
+
@authz.nonce = req.nonce
|
51
|
+
|
52
|
+
@authz.openid = req.scope.include?('openid')
|
53
|
+
if req.code_challenge && req.code_challenge_method
|
54
|
+
@authz.code_challenge = req.code_challenge
|
55
|
+
@authz.code_challenge_method = req.code_challenge_method || 'plain'
|
56
|
+
next req.bad_request!(:invalid_request, 'Invalid PKCE parameters') unless @authz.pkce_valid_request?
|
57
|
+
end
|
58
|
+
|
59
|
+
@storage.put_authorization(@authz)
|
60
|
+
res.code = @authz.code
|
61
|
+
|
62
|
+
@logger&.debug(Himari::LogLine.new('OidcAuthorizationEndpoint: grant code', req: env['himari.request_as_log'], client: @client.as_log, claims: @authz.claims, code: @authz.code))
|
63
|
+
end
|
64
|
+
|
65
|
+
# if requested_response_types.include?(:token)
|
66
|
+
# token = AccessToken.from_authz(@authz)
|
67
|
+
# @storage.put_token(token)
|
68
|
+
# res.access_token = token.format.to_s
|
69
|
+
# end
|
70
|
+
|
71
|
+
# if requested_response_types.include?(:id_token)
|
72
|
+
# @id_token.nonce = req.nonce
|
73
|
+
# res.id_token = @id_token.to_jwt
|
74
|
+
# end
|
75
|
+
|
76
|
+
@logger&.info(Himari::LogLine.new('OidcAuthorizationEndpoint: authorized', req: env['himari.request_as_log'], client: @client.as_log, claims: @authz.claims, redirect_uri: @authz.redirect_uri, code_dgst: Digest::SHA256.hexdigest(@authz.code)))
|
77
|
+
res.approve!
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module Himari
|
2
|
+
module Services
|
3
|
+
class OidcProviderMetadataEndpoint
|
4
|
+
# @param signing_key_provider [Himari::ProviderChain<Himari::SigningKey>]
|
5
|
+
def initialize(signing_key_provider:, issuer:)
|
6
|
+
@signing_key_provider = signing_key_provider
|
7
|
+
@issuer = issuer
|
8
|
+
end
|
9
|
+
|
10
|
+
def app
|
11
|
+
self
|
12
|
+
end
|
13
|
+
|
14
|
+
def call(env)
|
15
|
+
Handler.new(signing_key_provider: @signing_key_provider, issuer: @issuer, env: env).response
|
16
|
+
end
|
17
|
+
|
18
|
+
class Handler
|
19
|
+
class InvalidToken < StandardError; end
|
20
|
+
|
21
|
+
def initialize(signing_key_provider:, issuer:, env:)
|
22
|
+
@signing_key_provider = signing_key_provider
|
23
|
+
@issuer = issuer
|
24
|
+
@env = env
|
25
|
+
end
|
26
|
+
|
27
|
+
def metadata
|
28
|
+
signing_keys = @signing_key_provider.collect()
|
29
|
+
{
|
30
|
+
issuer: @issuer,
|
31
|
+
authorization_endpoint: "#{@issuer}/oidc/authorize",
|
32
|
+
token_endpoint: "#{@issuer}/public/oidc/token",
|
33
|
+
userinfo_endpoint: "#{@issuer}/public/oidc/userinfo",
|
34
|
+
jwks_uri: "#{@issuer}/public/jwks",
|
35
|
+
scopes_supported: %w(openid),
|
36
|
+
response_types_supported: ['code'], # violation: dynamic OpenID Provider MUST support code, id_token, token+id_token
|
37
|
+
subject_types_supported: ['public'],
|
38
|
+
id_token_signing_alg_values_supported: signing_keys.map(&:alg).uniq.sort,
|
39
|
+
claims_supported: %w(sub iss iat nbf exp),
|
40
|
+
}
|
41
|
+
end
|
42
|
+
|
43
|
+
def response
|
44
|
+
# https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata
|
45
|
+
return [404, {'Content-Type' => 'application/json'}, ['{"error": "not_found"}']] unless @env['REQUEST_METHOD'] == 'GET'
|
46
|
+
|
47
|
+
[
|
48
|
+
200,
|
49
|
+
{'Content-Type' => 'application/json; charset=utf-8'},
|
50
|
+
[JSON.pretty_generate(metadata), "\n"],
|
51
|
+
]
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
require 'rack/oauth2'
|
2
|
+
require 'digest/sha2'
|
3
|
+
require 'openid_connect'
|
4
|
+
require 'himari/access_token'
|
5
|
+
require 'himari/id_token'
|
6
|
+
|
7
|
+
module Himari
|
8
|
+
module Services
|
9
|
+
class OidcTokenEndpoint
|
10
|
+
class SigningKeyMissing < StandardError; end
|
11
|
+
|
12
|
+
# @param client_provider [Himari::ProviderChain<Himari::ClientRegistration>]
|
13
|
+
# @param signing_key_provider [Himari::ProviderChain<Himari::SigningKey>]
|
14
|
+
# @param storage [Himari::Storages::Base]
|
15
|
+
# @param issuer [String]
|
16
|
+
# @param logger [Logger]
|
17
|
+
def initialize(client_provider:, signing_key_provider:, storage:, issuer:, logger: nil)
|
18
|
+
@client_provider = client_provider
|
19
|
+
@signing_key_provider = signing_key_provider
|
20
|
+
@storage = storage
|
21
|
+
@issuer = issuer
|
22
|
+
@logger = logger
|
23
|
+
end
|
24
|
+
|
25
|
+
def call(env)
|
26
|
+
app(env).call(env)
|
27
|
+
rescue Rack::OAuth2::Server::Abstract::Error => e
|
28
|
+
@logger&.warn(Himari::LogLine.new('OidcTokenEndpoint: returning error', req: env['himari.request_as_log'], client: client.as_log, err: e.class.inspect, err_content: e.protocol_params))
|
29
|
+
e.finish
|
30
|
+
end
|
31
|
+
|
32
|
+
def app(env)
|
33
|
+
Rack::OAuth2::Server::Token.new do |req, res|
|
34
|
+
code_dgst = req.code ? Digest::SHA256.hexdigest(req.code) : nil
|
35
|
+
client = @client_provider.find(id: req.client_id)
|
36
|
+
unless client
|
37
|
+
@logger&.warn(Himari::LogLine.new('OidcTokenEndpoint: invalid_client, no client registration', req: env['himari.request_as_log'], client_id: req.client_id, code_dgst: code_dgst))
|
38
|
+
next req.invalid_client!
|
39
|
+
end
|
40
|
+
unless client.match_secret?(req.client_secret)
|
41
|
+
@logger&.warn(Himari::LogLine.new('OidcTokenEndpoint: invalid_client, client secret mismatch', req: env['himari.request_as_log'], client: client.as_log, code_dgst: code_dgst))
|
42
|
+
next req.invalid_client!
|
43
|
+
end
|
44
|
+
|
45
|
+
case req.grant_type
|
46
|
+
when :authorization_code
|
47
|
+
authz = @storage.find_authorization(req.code)
|
48
|
+
unless authz
|
49
|
+
@logger&.warn(Himari::LogLine.new('OidcTokenEndpoint: invalid_grant, no grant code found', req: env['himari.request_as_log'], client: client.as_log))
|
50
|
+
next req.invalid_grant!
|
51
|
+
end
|
52
|
+
unless authz.valid_redirect_uri?(req.redirect_uri)
|
53
|
+
@logger&.warn(Himari::LogLine.new('OidcTokenEndpoint: invalid_grant, redirect_uri mismatch', req: env['himari.request_as_log'], client: client.as_log, grant: authz.as_log))
|
54
|
+
next req.invalid_grant!
|
55
|
+
end
|
56
|
+
if authz.expiry <= Time.now.to_i
|
57
|
+
@logger&.warn(Himari::LogLine.new('OidcTokenEndpoint: invalid_grant, expired grant', req: env['himari.request_as_log'], client: client.as_log, grant: authz.as_log))
|
58
|
+
next req.invalid_grant!
|
59
|
+
end
|
60
|
+
if authz.pkce? && !req.verify_code_verifier!(authz.code_challenge, authz.code_challenge_method)
|
61
|
+
# :nocov:
|
62
|
+
@logger&.warn(Himari::LogLine.new('OidcTokenEndpoint: invalid_grant, invalid pkce', req: env['himari.request_as_log'], client: client.as_log, grant: authz.as_log))
|
63
|
+
next req.invalid_grant!
|
64
|
+
# :nocov:
|
65
|
+
end
|
66
|
+
|
67
|
+
token = AccessToken.from_authz(authz)
|
68
|
+
@storage.put_token(token)
|
69
|
+
res.access_token = token.to_bearer
|
70
|
+
|
71
|
+
if authz.openid
|
72
|
+
signing_key = @signing_key_provider.find(group: client.preferred_key_group, active: true)
|
73
|
+
raise SigningKeyMissing unless signing_key
|
74
|
+
res.id_token = IdToken.from_authz(authz, signing_key: signing_key, access_token: token.format.to_s, issuer: @issuer).to_jwt
|
75
|
+
end
|
76
|
+
|
77
|
+
@storage.delete_authorization(authz)
|
78
|
+
@logger&.info(Himari::LogLine.new('OidcTokenEndpoint: issued', req: env['himari.request_as_log'], client: client.as_log, grant: authz.as_log, token: token.as_log, signing_key_kid: signing_key&.id))
|
79
|
+
else
|
80
|
+
req.unsupported_response_type!
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require 'himari/access_token'
|
2
|
+
require 'himari/log_line'
|
3
|
+
|
4
|
+
module Himari
|
5
|
+
module Services
|
6
|
+
class OidcUserinfoEndpoint
|
7
|
+
# @param storage [Himari::Storages::Base]
|
8
|
+
# @param logger [Logger]
|
9
|
+
def initialize(storage:, logger: nil)
|
10
|
+
@storage = storage
|
11
|
+
@logger = logger
|
12
|
+
end
|
13
|
+
|
14
|
+
def app
|
15
|
+
self
|
16
|
+
end
|
17
|
+
|
18
|
+
def call(env)
|
19
|
+
Handler.new(storage: @storage, env: env, logger: @logger).response
|
20
|
+
end
|
21
|
+
|
22
|
+
class Handler
|
23
|
+
class InvalidToken < StandardError; end
|
24
|
+
|
25
|
+
def initialize(storage:, env:, logger:)
|
26
|
+
@storage = storage
|
27
|
+
@env = env
|
28
|
+
@logger = logger
|
29
|
+
end
|
30
|
+
|
31
|
+
def response
|
32
|
+
# https://openid.net/specs/openid-connect-core-1_0.html#UserInfo
|
33
|
+
return [404, {'Content-Type' => 'application/json'}, ['{"error": "not_found"}']] unless %w(GET POST).include?(@env['REQUEST_METHOD'])
|
34
|
+
|
35
|
+
raise InvalidToken unless given_token
|
36
|
+
given_parsed_token = Himari::AccessToken::Format.parse(given_token)
|
37
|
+
|
38
|
+
token = @storage.find_token(given_parsed_token.handler)
|
39
|
+
raise InvalidToken unless token
|
40
|
+
token.verify_expiry!()
|
41
|
+
token.verify_secret!(given_parsed_token.secret)
|
42
|
+
|
43
|
+
@logger&.info(Himari::LogLine.new('OidcUserinfoEndpoint: returning', req: @env['himari.request_as_log'], token: token.as_log))
|
44
|
+
[
|
45
|
+
200,
|
46
|
+
{'Content-Type' => 'application/json; charset=utf-8'},
|
47
|
+
[JSON.pretty_generate(token.claims), "\n"],
|
48
|
+
]
|
49
|
+
rescue InvalidToken, Himari::AccessToken::SecretIncorrect, Himari::AccessToken::InvalidFormat, Himari::AccessToken::TokenExpired => e
|
50
|
+
@logger&.warn(Himari::LogLine.new('OidcUserinfoEndpoint: invalid_token', req: @env['himari.request_as_log'], err: e.class.inspect, token: token&.as_log))
|
51
|
+
[
|
52
|
+
401,
|
53
|
+
{'Content-Type' => 'application/json', 'WWW-Authenticate' => 'error="invalid_token", error_description="invalid access token"'},
|
54
|
+
[JSON.pretty_generate(error: 'invalid_token'), "\n"],
|
55
|
+
]
|
56
|
+
end
|
57
|
+
|
58
|
+
def given_token
|
59
|
+
# Only supports Authorization Request Header Field method https://www.rfc-editor.org/rfc/rfc6750.html#section-2.1
|
60
|
+
@given_token ||= begin
|
61
|
+
ah = @env['HTTP_AUTHORIZATION']
|
62
|
+
method, token = ah&.split(/\s+/, 2) # https://www.rfc-editor.org/rfc/rfc9110#name-credentials
|
63
|
+
if method&.downcase == 'bearer' && token && !token.empty?
|
64
|
+
token
|
65
|
+
else
|
66
|
+
nil
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
require 'himari/log_line'
|
2
|
+
require 'himari/decisions/authentication'
|
3
|
+
require 'himari/decisions/claims'
|
4
|
+
require 'himari/middlewares/authentication_rule'
|
5
|
+
require 'himari/middlewares/claims_rule'
|
6
|
+
require 'himari/rule_processor'
|
7
|
+
require 'himari/session_data'
|
8
|
+
require 'himari/provider_chain'
|
9
|
+
|
10
|
+
module Himari
|
11
|
+
module Services
|
12
|
+
class UpstreamAuthentication
|
13
|
+
class UnauthorizedError < StandardError
|
14
|
+
# @param result [Himari::RuleProcessor::Result]
|
15
|
+
def initialize(result)
|
16
|
+
@result = result
|
17
|
+
super("Unauthorized")
|
18
|
+
end
|
19
|
+
|
20
|
+
attr_reader :result
|
21
|
+
|
22
|
+
def as_log
|
23
|
+
result.as_log
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
Result = Struct.new(:claims_result, :authn_result, :session_data) do
|
28
|
+
def as_log
|
29
|
+
{
|
30
|
+
claims: session_data&.claims,
|
31
|
+
decision: {
|
32
|
+
claims: claims_result&.as_log&.reject{ |k,_v| %i(allowed explicit_deny).include?(k) },
|
33
|
+
authentication: authn_result&.as_log,
|
34
|
+
},
|
35
|
+
}
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# @param auth [Hash] Omniauth Auth Hash
|
40
|
+
# @param claims_rules [Array<Himari::Rule>] Claims Rules
|
41
|
+
# @param authn_rules [Array<Himari::Rule>] Authentication Rules
|
42
|
+
# @param logger [Logger]
|
43
|
+
def initialize(auth:, request: nil, claims_rules: [], authn_rules: [], logger: nil)
|
44
|
+
@request = request
|
45
|
+
@auth = auth
|
46
|
+
@claims_rules = claims_rules
|
47
|
+
@authn_rules = authn_rules
|
48
|
+
@logger = logger
|
49
|
+
end
|
50
|
+
|
51
|
+
# @param request [Rack::Request]
|
52
|
+
def self.from_request(request)
|
53
|
+
new(
|
54
|
+
auth: request.env.fetch('omniauth.auth'),
|
55
|
+
request: request,
|
56
|
+
claims_rules: Himari::ProviderChain.new(request.env[Himari::Middlewares::ClaimsRule::RACK_KEY] || []).collect,
|
57
|
+
authn_rules: Himari::ProviderChain.new(request.env[Himari::Middlewares::AuthenticationRule::RACK_KEY] || []).collect,
|
58
|
+
logger: request.env['rack.logger'],
|
59
|
+
)
|
60
|
+
end
|
61
|
+
|
62
|
+
def provider
|
63
|
+
@auth&.fetch(:provider)
|
64
|
+
end
|
65
|
+
|
66
|
+
def perform
|
67
|
+
@logger&.debug(Himari::LogLine.new('UpstreamAuthentication: perform', objid: self.object_id.to_s(16), uid: @auth[:uid], provider: @auth[:provider]))
|
68
|
+
claims_result = make_claims()
|
69
|
+
session_data = claims_result.decision.output
|
70
|
+
|
71
|
+
authn_result = check_authn(claims_result, session_data)
|
72
|
+
|
73
|
+
|
74
|
+
result = Result.new(claims_result, authn_result, session_data)
|
75
|
+
@logger&.debug(Himari::LogLine.new('UpstreamAuthentication: result', objid: self.object_id.to_s(16), uid: @auth[:uid], provider: @auth[:provider], result: result.as_log))
|
76
|
+
result
|
77
|
+
end
|
78
|
+
|
79
|
+
def make_claims
|
80
|
+
context = Himari::Decisions::Claims::Context.new(request: @request, auth: @auth).freeze
|
81
|
+
result = Himari::RuleProcessor.new(context, Himari::Decisions::Claims.new).run(@claims_rules)
|
82
|
+
|
83
|
+
@logger&.debug(Himari::LogLine.new('UpstreamAuthentication: claims', objid: self.object_id.to_s(16), uid: @auth[:uid], provider: @auth[:provider], claims_result: result.as_log))
|
84
|
+
|
85
|
+
begin
|
86
|
+
claims = result.decision&.output&.claims
|
87
|
+
raise UnauthorizedError.new(Result.new(result, nil, nil)) unless claims
|
88
|
+
rescue Himari::Decisions::Claims::UninitializedError
|
89
|
+
raise UnauthorizedError.new(Result.new(result, nil, nil))
|
90
|
+
end
|
91
|
+
|
92
|
+
result
|
93
|
+
end
|
94
|
+
|
95
|
+
def check_authn(claims_result, session_data)
|
96
|
+
context = Himari::Decisions::Authentication::Context.new(provider: provider, claims: session_data.claims, user_data: session_data.user_data, request: @request).freeze
|
97
|
+
result = Himari::RuleProcessor.new(context, Himari::Decisions::Authentication.new).run(@authn_rules)
|
98
|
+
|
99
|
+
@logger&.debug(Himari::LogLine.new('UpstreamAuthentication: authentication', objid: self.object_id.to_s(16), uid: @auth[:uid], provider: @auth[:provider], authn_result: result.as_log))
|
100
|
+
|
101
|
+
raise UnauthorizedError.new(Result.new(claims_result, result, nil)) unless result.allowed
|
102
|
+
result
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,128 @@
|
|
1
|
+
require 'digest/sha2'
|
2
|
+
require 'base64'
|
3
|
+
module Himari
|
4
|
+
class SigningKey
|
5
|
+
class AlgUnknown < StandardError; end
|
6
|
+
class OperationInvalid < StandardError; end
|
7
|
+
|
8
|
+
def initialize(id:, pkey:, alg: nil, inactive: false, group: nil)
|
9
|
+
@id = id
|
10
|
+
@pkey = pkey
|
11
|
+
@alg = alg
|
12
|
+
@inactive = inactive
|
13
|
+
@group = group
|
14
|
+
end
|
15
|
+
|
16
|
+
attr_reader :id, :pkey, :group
|
17
|
+
|
18
|
+
|
19
|
+
def active?
|
20
|
+
!@inactive
|
21
|
+
end
|
22
|
+
|
23
|
+
def match_hint?(id: nil, active: nil, group: nil)
|
24
|
+
result = true
|
25
|
+
|
26
|
+
result &&= if id
|
27
|
+
id == self.id
|
28
|
+
else
|
29
|
+
true
|
30
|
+
end
|
31
|
+
|
32
|
+
result &&= if !active.nil?
|
33
|
+
active == self.active?
|
34
|
+
else
|
35
|
+
true
|
36
|
+
end
|
37
|
+
|
38
|
+
result &&= if group
|
39
|
+
group == self.group
|
40
|
+
else
|
41
|
+
true
|
42
|
+
end
|
43
|
+
|
44
|
+
result
|
45
|
+
end
|
46
|
+
|
47
|
+
def alg
|
48
|
+
@alg ||= inferred_alg
|
49
|
+
end
|
50
|
+
|
51
|
+
def inferred_alg
|
52
|
+
# https://datatracker.ietf.org/doc/html/rfc7518#section-3.1
|
53
|
+
case pkey
|
54
|
+
when OpenSSL::PKey::RSA
|
55
|
+
'RS256'
|
56
|
+
when OpenSSL::PKey::EC
|
57
|
+
case ec_crv
|
58
|
+
when 'P-256'; 'ES256'
|
59
|
+
when 'P-384'; 'ES384'
|
60
|
+
when 'P-521'; 'ES512'
|
61
|
+
else
|
62
|
+
raise AlgUnknown
|
63
|
+
end
|
64
|
+
else
|
65
|
+
raise AlgUnknown
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def hash_function
|
70
|
+
case alg
|
71
|
+
when 'ES256', 'RS256'; Digest::SHA256
|
72
|
+
when 'ES384'; Digest::SHA384
|
73
|
+
when 'ES512'; Digest::SHA512
|
74
|
+
else
|
75
|
+
raise AlgUnknown
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def ec_crv
|
80
|
+
raise OperationInvalid, "this key is not EC" unless pkey.is_a?(OpenSSL::PKey::EC)
|
81
|
+
# https://www.rfc-editor.org/rfc/rfc8422.html#appendix-A
|
82
|
+
case pkey.group.curve_name
|
83
|
+
when 'prime256v1', 'secp256r1'
|
84
|
+
'P-256'
|
85
|
+
when 'secp384r1'
|
86
|
+
'P-384'
|
87
|
+
when 'secp521r1'
|
88
|
+
'P-521'
|
89
|
+
else
|
90
|
+
raise AlgUnknown
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def as_jwk
|
95
|
+
# https://www.rfc-editor.org/rfc/rfc7517#section-4
|
96
|
+
case pkey
|
97
|
+
when OpenSSL::PKey::EC # https://www.rfc-editor.org/rfc/rfc7518#section-6.2
|
98
|
+
# https://www.secg.org/sec1-v2.pdf - 2.3.3. Elliptic-Curve-Point-to-Octet-String Conversion
|
99
|
+
xy = pkey.public_key.to_octet_string(:uncompressed) # 0x04 || X || Y
|
100
|
+
len = pkey.group.degree/8
|
101
|
+
raise unless xy[0] == "\x04".b && xy.size == ((len*2)+1)
|
102
|
+
x = xy[1,len]
|
103
|
+
y = xy[1+len,len]
|
104
|
+
|
105
|
+
{
|
106
|
+
kid: id,
|
107
|
+
kty: 'EC',
|
108
|
+
crv: ec_crv,
|
109
|
+
use: "sig",
|
110
|
+
alg: alg,
|
111
|
+
x: Base64.urlsafe_encode64(OpenSSL::BN.new(x, 2).to_s(2)).gsub(/\n|=/, ''),
|
112
|
+
y: Base64.urlsafe_encode64(OpenSSL::BN.new(y, 2).to_s(2)).gsub(/\n|=/, ''),
|
113
|
+
}
|
114
|
+
when OpenSSL::PKey::RSA # https://www.rfc-editor.org/rfc/rfc7518#section-6.3
|
115
|
+
{
|
116
|
+
kid: id,
|
117
|
+
kty: 'RSA',
|
118
|
+
use: "sig",
|
119
|
+
alg: alg,
|
120
|
+
n: Base64.urlsafe_encode64(pkey.n.to_s(2)).gsub(/=+/,''),
|
121
|
+
e: Base64.urlsafe_encode64(pkey.e.to_s(2)).gsub(/=+/,''),
|
122
|
+
}
|
123
|
+
else
|
124
|
+
raise AlgUnknown
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'himari/authorization_code'
|
2
|
+
require 'himari/access_token'
|
3
|
+
|
4
|
+
module Himari
|
5
|
+
module Storages
|
6
|
+
module Base
|
7
|
+
class Conflict < StandardError; end
|
8
|
+
|
9
|
+
def find_authorization(code)
|
10
|
+
content = read('authz', code)
|
11
|
+
content && AuthorizationCode.new(**content)
|
12
|
+
end
|
13
|
+
|
14
|
+
def put_authorization(authz, overwrite: false)
|
15
|
+
write('authz', authz.code, authz.as_json, overwrite: overwrite)
|
16
|
+
end
|
17
|
+
|
18
|
+
def delete_authorization(authz)
|
19
|
+
delete_authorization_by_code(authz.code)
|
20
|
+
end
|
21
|
+
|
22
|
+
def delete_authorization_by_code(code)
|
23
|
+
delete('authz', code)
|
24
|
+
end
|
25
|
+
|
26
|
+
def find_token(handler)
|
27
|
+
content = read('token', handler)
|
28
|
+
content && AccessToken.new(**content)
|
29
|
+
end
|
30
|
+
|
31
|
+
def put_token(token, overwrite: false)
|
32
|
+
write('token', token.handler, token.as_json, overwrite: overwrite)
|
33
|
+
end
|
34
|
+
|
35
|
+
def delete_token(token)
|
36
|
+
delete_authorization_by_token(token.handler)
|
37
|
+
end
|
38
|
+
|
39
|
+
def delete_token_by_handler(handler)
|
40
|
+
delete('token', handler)
|
41
|
+
end
|
42
|
+
|
43
|
+
|
44
|
+
private def write(kind, key, content, overwrite: false)
|
45
|
+
raise NotImplementedError
|
46
|
+
end
|
47
|
+
|
48
|
+
private def read(kind, key)
|
49
|
+
raise NotImplementedError
|
50
|
+
end
|
51
|
+
|
52
|
+
private def delete(kind, key)
|
53
|
+
raise NotImplementedError
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'himari/storages/base'
|
2
|
+
|
3
|
+
module Himari
|
4
|
+
module Storages
|
5
|
+
class Filesystem
|
6
|
+
include Himari::Storages::Base
|
7
|
+
|
8
|
+
def initialize(path)
|
9
|
+
@path = path
|
10
|
+
end
|
11
|
+
|
12
|
+
attr_reader :path
|
13
|
+
|
14
|
+
private def write(kind, key, content, overwrite: false)
|
15
|
+
dir = File.join(@path, kind)
|
16
|
+
path = File.join(dir, key)
|
17
|
+
Dir.mkdir(dir) unless Dir.exist?(dir)
|
18
|
+
raise Himari::Storages::Base::Conflict if File.exist?(path)
|
19
|
+
File.write(path, "#{JSON.pretty_generate(content)}\n")
|
20
|
+
nil
|
21
|
+
end
|
22
|
+
|
23
|
+
private def read(kind, key)
|
24
|
+
path = File.join(@path, kind, key)
|
25
|
+
JSON.parse(File.read(path), symbolize_names: true)
|
26
|
+
rescue Errno::ENOENT
|
27
|
+
return nil
|
28
|
+
end
|
29
|
+
|
30
|
+
private def delete(kind, key)
|
31
|
+
path = File.join(@path, kind, key)
|
32
|
+
File.unlink(path) if File.exist?(path)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|