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.
Files changed (47) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/Gemfile +11 -0
  4. data/Gemfile.lock +152 -0
  5. data/LICENSE.txt +21 -0
  6. data/Rakefile +8 -0
  7. data/himari.gemspec +44 -0
  8. data/lib/himari/access_token.rb +119 -0
  9. data/lib/himari/app.rb +193 -0
  10. data/lib/himari/authorization_code.rb +83 -0
  11. data/lib/himari/client_registration.rb +47 -0
  12. data/lib/himari/config.rb +39 -0
  13. data/lib/himari/decisions/authentication.rb +16 -0
  14. data/lib/himari/decisions/authorization.rb +48 -0
  15. data/lib/himari/decisions/base.rb +63 -0
  16. data/lib/himari/decisions/claims.rb +58 -0
  17. data/lib/himari/id_token.rb +57 -0
  18. data/lib/himari/item_provider.rb +11 -0
  19. data/lib/himari/item_providers/static.rb +20 -0
  20. data/lib/himari/log_line.rb +9 -0
  21. data/lib/himari/middlewares/authentication_rule.rb +24 -0
  22. data/lib/himari/middlewares/authorization_rule.rb +24 -0
  23. data/lib/himari/middlewares/claims_rule.rb +24 -0
  24. data/lib/himari/middlewares/client.rb +24 -0
  25. data/lib/himari/middlewares/config.rb +24 -0
  26. data/lib/himari/middlewares/signing_key.rb +24 -0
  27. data/lib/himari/provider_chain.rb +26 -0
  28. data/lib/himari/rule.rb +7 -0
  29. data/lib/himari/rule_processor.rb +81 -0
  30. data/lib/himari/services/downstream_authorization.rb +73 -0
  31. data/lib/himari/services/jwks_endpoint.rb +40 -0
  32. data/lib/himari/services/oidc_authorization_endpoint.rb +82 -0
  33. data/lib/himari/services/oidc_provider_metadata_endpoint.rb +56 -0
  34. data/lib/himari/services/oidc_token_endpoint.rb +86 -0
  35. data/lib/himari/services/oidc_userinfo_endpoint.rb +73 -0
  36. data/lib/himari/services/upstream_authentication.rb +106 -0
  37. data/lib/himari/session_data.rb +7 -0
  38. data/lib/himari/signing_key.rb +128 -0
  39. data/lib/himari/storages/base.rb +57 -0
  40. data/lib/himari/storages/filesystem.rb +36 -0
  41. data/lib/himari/storages/memory.rb +31 -0
  42. data/lib/himari/version.rb +5 -0
  43. data/lib/himari.rb +4 -0
  44. data/public/public/index.css +74 -0
  45. data/sig/himari.rbs +4 -0
  46. data/views/login.erb +37 -0
  47. 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,7 @@
1
+ module Himari
2
+ SessionData = Struct.new(:claims, :user_data, keyword_init: true) do
3
+ def as_log
4
+ {claims: claims}
5
+ end
6
+ end
7
+ 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