oauth2c 0.1.0

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