himari 0.1.0

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