oauth2c 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.
@@ -0,0 +1,92 @@
1
+ # Copyright 2017 Doximity, Inc. <support@doximity.com>
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ require "uri"
16
+ require "http"
17
+ require "json"
18
+
19
+ module OAuth2c
20
+ class Agent
21
+ using Refinements
22
+
23
+ ConfigError = Class.new(StandardError)
24
+
25
+ def initialize(authz_url: nil, token_url:, client_id:, client_secret: nil, redirect_uri: nil)
26
+ @authz_url = authz_url && authz_url.chomp("/")
27
+ @token_url = token_url && token_url.chomp("/")
28
+ @client_id = client_id
29
+ @client_secret = client_secret
30
+ @redirect_uri = redirect_uri
31
+
32
+ @http_client = HTTP.nodelay
33
+ .basic_auth(user: @client_id, pass: @client_secret)
34
+ .accept("application/json")
35
+ .headers("Content-Type": "application/x-www-form-urlencoded; encoding=UTF-8")
36
+ end
37
+
38
+ def authz_url(response_type:, state:, scope: [], **params)
39
+ if @authz_url.nil?
40
+ raise ConfigError, "authz_url not informed for client"
41
+ end
42
+
43
+ if @redirect_uri.nil?
44
+ raise ConfigError, "redirect_uri not informed for client"
45
+ end
46
+
47
+ params = {
48
+ client_id: @client_id,
49
+ redirect_uri: @redirect_uri,
50
+ response_type: response_type,
51
+ state: state,
52
+ scope: normalize_scope(scope),
53
+ **params
54
+ }
55
+
56
+ url = URI.parse(@authz_url)
57
+ url.query = URI.encode_www_form(params.to_a)
58
+ url.to_s
59
+ end
60
+
61
+ def token(grant_type:, scope: [], include_redirect_uri: false, **params)
62
+ params = {
63
+ grant_type: grant_type,
64
+ scope: normalize_scope(scope),
65
+ **params,
66
+ }
67
+
68
+ if include_redirect_uri
69
+ params[:redirect_uri] = @redirect_uri
70
+ end
71
+
72
+ response = @http_client.post(@token_url, body: URI.encode_www_form(params))
73
+
74
+ [ response.status.success?, JSON.parse(response.body) ]
75
+ end
76
+
77
+ private
78
+
79
+ def normalize_scope(scope)
80
+ case scope
81
+ when "", [], NilClass
82
+ nil
83
+ when String
84
+ scope
85
+ when Array
86
+ scope.join(" ")
87
+ else
88
+ raise ArgumentError, "invalid scope: #{scope.inspect}"
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,46 @@
1
+ # Copyright 2017 Doximity, Inc. <support@doximity.com>
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ require "thread"
16
+
17
+ module OAuth2c
18
+ module Cache
19
+ module Backends
20
+ class InMemoryLRU
21
+ def initialize(max_size)
22
+ @max_size = max_size
23
+ @store = {}
24
+ @mtx = Mutex.new
25
+ end
26
+
27
+ def lookup(key)
28
+ @mtx.synchronize do
29
+ return nil unless @store.has_key?(key)
30
+ @store[key] = @store.delete(key)
31
+ end
32
+ end
33
+
34
+ def store(key, bucket)
35
+ @mtx.synchronize do
36
+ @store[key] = bucket
37
+
38
+ if @store.size > @max_size
39
+ @store.shift
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,31 @@
1
+ # Copyright 2017 Doximity, Inc. <support@doximity.com>
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ require "thread"
16
+
17
+ module OAuth2c
18
+ module Cache
19
+ module Backends
20
+ class Null
21
+ def lookup(key)
22
+ nil
23
+ end
24
+
25
+ def store(key, bucket)
26
+ nil
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,56 @@
1
+ # Copyright 2017 Doximity, Inc. <support@doximity.com>
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ require "json"
16
+
17
+ module OAuth2c
18
+ module Cache
19
+ module Backends
20
+ class Redis
21
+ using Refinements
22
+
23
+ def initialize(redis, namespace = nil)
24
+ @redis = redis
25
+ @namespace = namespace
26
+ end
27
+
28
+ def lookup(key)
29
+ access_token_data, scope_data = @redis.mget("#{fq_key(key)}:access_token", "#{fq_key(key)}:scope")
30
+ return if access_token_data.nil? || scope_data.nil?
31
+
32
+ access_token = AccessToken.new(**JSON.load(access_token_data).symbolize_keys)
33
+ Store::Bucket.new(access_token, JSON.load(scope_data))
34
+ end
35
+
36
+ def store(key, bucket)
37
+ access_token = bucket.access_token
38
+
39
+ @redis.mset(
40
+ "#{fq_key(key)}:access_token", JSON.dump(access_token.attributes),
41
+ "#{fq_key(key)}:scope", JSON.dump(bucket.scope),
42
+ )
43
+
44
+ @redis.expire("#{fq_key(key)}:access_token", access_token.expires_in)
45
+ @redis.expire("#{fq_key(key)}:scope", access_token.expires_in)
46
+ end
47
+
48
+ private
49
+
50
+ def fq_key(key)
51
+ @namespace ? "#{@namespace}:#{key}" : key
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,23 @@
1
+ # Copyright 2017 Doximity, Inc. <support@doximity.com>
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ module OAuth2c
16
+ module Cache
17
+ module Backends
18
+ autoload :Null, "oauth2c/cache/backends/null"
19
+ autoload :InMemoryLRU, "oauth2c/cache/backends/in_memory_lru"
20
+ autoload :Redis, "oauth2c/cache/backends/redis"
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,54 @@
1
+ # Copyright 2017 Doximity, Inc. <support@doximity.com>
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ require "forwardable"
16
+
17
+ module OAuth2c
18
+ module Cache
19
+ class Manager
20
+ extend Forwardable
21
+
22
+ def initialize(client, cache_backend)
23
+ @client = client
24
+ @cache = Cache::Store.new(cache_backend)
25
+ end
26
+
27
+ def_delegators :@cache, :cached?, :cached
28
+
29
+ def method_missing(name, key, *args, **opts)
30
+ grant = @client.public_send(name, *args, **opts)
31
+ CacheProxy.new(@cache, key, grant)
32
+ end
33
+
34
+ class CacheProxy < BasicObject
35
+ def initialize(cache, key, grant)
36
+ @cache = cache
37
+ @key = key
38
+ @grant = grant
39
+ end
40
+
41
+ def method_missing(name, *args)
42
+ @grant.public_send(name, *args)
43
+ end
44
+
45
+ def token(*args)
46
+ @cache.issue(@key, scope: @grant.scope) do |new_scope|
47
+ @grant.update_scope(new_scope)
48
+ @grant.token(*args)
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,48 @@
1
+ # Copyright 2017 Doximity, Inc. <support@doximity.com>
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ module OAuth2c
16
+ module Cache
17
+ class Store
18
+ Bucket = Struct.new(:access_token, :scope)
19
+
20
+ def initialize(backend)
21
+ @backend = backend
22
+ end
23
+
24
+ def cached?(key, scope: [])
25
+ cached(key, scope: scope) ? true : false
26
+ end
27
+
28
+ def cached(key, scope: [])
29
+ cache = @backend.lookup(key)
30
+ return false if cache.nil?
31
+ return false unless scope.all? { |s| cache.scope.include?(s) }
32
+
33
+ if cache.access_token.expires_at >= Time.now
34
+ cache.access_token
35
+ end
36
+ end
37
+
38
+ def issue(key, scope:, &block)
39
+ cached = @backend.lookup(key)
40
+ scope = cached[:scope] | scope if cached
41
+
42
+ access_token = block.call(scope)
43
+ @backend.store(key, Bucket.new(access_token, scope))
44
+ access_token
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,21 @@
1
+ # Copyright 2017 Doximity, Inc. <support@doximity.com>
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ module OAuth2c
16
+ module Cache
17
+ autoload :Backends, "oauth2c/cache/backends"
18
+ autoload :Manager, "oauth2c/cache/manager"
19
+ autoload :Store, "oauth2c/cache/store"
20
+ end
21
+ end
@@ -0,0 +1,51 @@
1
+ # Copyright 2017 Doximity, Inc. <support@doximity.com>
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ module OAuth2c
16
+ class Client
17
+ using Refinements
18
+
19
+ attr_reader(
20
+ :authz_url,
21
+ :token_url,
22
+ :client_id,
23
+ :client_secret,
24
+ :redirect_uri,
25
+ )
26
+
27
+ def initialize(authz_url: nil, token_url:, client_id:, client_secret: nil, redirect_uri: nil)
28
+ @authz_url = authz_url
29
+ @token_url = token_url
30
+ @client_id = client_id
31
+ @client_secret = client_secret
32
+ @redirect_uri = redirect_uri
33
+ end
34
+
35
+ def method_missing(name, *_, **opts)
36
+ Grants.const_get(name.to_s.camelize).new(build_agent, **opts)
37
+ end
38
+
39
+ private
40
+
41
+ def build_agent
42
+ Agent.new(
43
+ authz_url: @authz_url,
44
+ token_url: @token_url,
45
+ client_id: @client_id,
46
+ client_secret: @client_secret,
47
+ redirect_uri: @redirect_uri,
48
+ )
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,28 @@
1
+ # Copyright 2017 Doximity, Inc. <support@doximity.com>
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ module OAuth2c
16
+ class Error < StandardError
17
+ attr_accessor(
18
+ :error,
19
+ :error_description,
20
+ )
21
+
22
+ def initialize(error, error_description)
23
+ @error = error
24
+ @error_description = error_description
25
+ super("#{error}: #{error_description}")
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,80 @@
1
+ # Copyright 2017 Doximity, Inc. <support@doximity.com>
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ require "securerandom"
16
+ require "jwt"
17
+
18
+ module OAuth2c
19
+ module Grants
20
+ class Assertion < OAuth2c::TwoLegged::Base
21
+ class JWTProfile
22
+ def initialize(alg, key,
23
+ iss:,
24
+ aud:,
25
+ sub:,
26
+ jti: SecureRandom.uuid,
27
+ exp: Time.now + (5 * 60),
28
+ nbf: Time.now,
29
+ iat: Time.now,
30
+ **other_claims
31
+ )
32
+ @alg = alg
33
+ @key = key
34
+
35
+ @iss = iss
36
+ @aud = aud
37
+ @sub = sub
38
+ @jti = jti
39
+ @exp = exp
40
+ @nbf = nbf
41
+ @iat = iat
42
+
43
+ @other_claims = other_claims
44
+ end
45
+
46
+ def grant_type
47
+ "urn:ietf:params:oauth:grant-type:jwt-bearer"
48
+ end
49
+
50
+ def assertion
51
+ JWT.encode(claims, @key, @alg)
52
+ end
53
+
54
+ def claims
55
+ {
56
+ iss: @iss,
57
+ sub: @sub,
58
+ aud: @aud,
59
+ jti: @jti,
60
+ exp: @exp && @exp.utc.to_i,
61
+ nbf: @nbf && @nbf.utc.to_i,
62
+ iat: @iat && @iat.utc.to_i,
63
+ **@other_claims,
64
+ }
65
+ end
66
+ end
67
+
68
+ def initialize(agent, profile:, **opts)
69
+ super(agent, **opts)
70
+ @profile = profile
71
+ end
72
+
73
+ protected
74
+
75
+ def token_params
76
+ { grant_type: @profile.grant_type, assertion: @profile.assertion }
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,29 @@
1
+ # Copyright 2017 Doximity, Inc. <support@doximity.com>
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ module OAuth2c
16
+ module Grants
17
+ class AuthorizationCode < ThreeLegged::Base
18
+ protected
19
+
20
+ def authz_params
21
+ { response_type: "code" }
22
+ end
23
+
24
+ def token_params(code:, **_)
25
+ { grant_type: "authorization_code", code: code }
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,25 @@
1
+ # Copyright 2017 Doximity, Inc. <support@doximity.com>
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ module OAuth2c
16
+ module Grants
17
+ class ClientCredentials < OAuth2c::TwoLegged::Base
18
+ protected
19
+
20
+ def token_params
21
+ { grant_type: "client_credentials" }
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,33 @@
1
+ # Copyright 2017 Doximity, Inc. <support@doximity.com>
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ module OAuth2c
16
+ module Grants
17
+ class Implicit < OAuth2c::ThreeLegged::Base
18
+ using Refinements
19
+
20
+ def token(callback_url)
21
+ super(callback_url) do |_, fragment_params|
22
+ AccessToken.new(**fragment_params)
23
+ end
24
+ end
25
+
26
+ protected
27
+
28
+ def authz_params
29
+ { response_type: "token" }
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,30 @@
1
+ # Copyright 2017 Doximity, Inc. <support@doximity.com>
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ module OAuth2c
16
+ module Grants
17
+ class RefreshToken < OAuth2c::TwoLegged::Base
18
+ def initialize(agent, refresh_token:)
19
+ super(agent)
20
+ @refresh_token = refresh_token
21
+ end
22
+
23
+ protected
24
+
25
+ def token_params
26
+ { grant_type: "refresh_token", refresh_token: @refresh_token }
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,31 @@
1
+ # Copyright 2017 Doximity, Inc. <support@doximity.com>
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ module OAuth2c
16
+ module Grants
17
+ class ResourceOwnerCredentials < OAuth2c::TwoLegged::Base
18
+ def initialize(agent, username:, password:)
19
+ super(agent)
20
+ @username = username
21
+ @password = password
22
+ end
23
+
24
+ protected
25
+
26
+ def token_params
27
+ { grant_type: "password", username: @username, password: @password }
28
+ end
29
+ end
30
+ end
31
+ end