himari 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|