adal 1.0.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 (98) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +5 -0
  3. data/.rubocop.yml +7 -0
  4. data/.travis.yml +7 -0
  5. data/Gemfile +25 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +97 -0
  8. data/Rakefile +39 -0
  9. data/adal.gemspec +52 -0
  10. data/contributing.md +127 -0
  11. data/lib/adal.rb +24 -0
  12. data/lib/adal/authentication_context.rb +202 -0
  13. data/lib/adal/authentication_parameters.rb +126 -0
  14. data/lib/adal/authority.rb +165 -0
  15. data/lib/adal/cache_driver.rb +171 -0
  16. data/lib/adal/cached_token_response.rb +190 -0
  17. data/lib/adal/client_assertion.rb +63 -0
  18. data/lib/adal/client_assertion_certificate.rb +89 -0
  19. data/lib/adal/client_credential.rb +46 -0
  20. data/lib/adal/core_ext.rb +26 -0
  21. data/lib/adal/core_ext/hash.rb +34 -0
  22. data/lib/adal/jwt_parameters.rb +39 -0
  23. data/lib/adal/logger.rb +90 -0
  24. data/lib/adal/logging.rb +98 -0
  25. data/lib/adal/memory_cache.rb +95 -0
  26. data/lib/adal/mex_request.rb +52 -0
  27. data/lib/adal/mex_response.rb +141 -0
  28. data/lib/adal/noop_cache.rb +38 -0
  29. data/lib/adal/oauth_request.rb +76 -0
  30. data/lib/adal/request_parameters.rb +48 -0
  31. data/lib/adal/self_signed_jwt_factory.rb +96 -0
  32. data/lib/adal/templates/rst.13.xml.erb +35 -0
  33. data/lib/adal/templates/rst.2005.xml.erb +32 -0
  34. data/lib/adal/token_request.rb +231 -0
  35. data/lib/adal/token_response.rb +144 -0
  36. data/lib/adal/user_assertion.rb +57 -0
  37. data/lib/adal/user_credential.rb +152 -0
  38. data/lib/adal/user_identifier.rb +83 -0
  39. data/lib/adal/user_information.rb +49 -0
  40. data/lib/adal/util.rb +49 -0
  41. data/lib/adal/version.rb +36 -0
  42. data/lib/adal/wstrust_request.rb +100 -0
  43. data/lib/adal/wstrust_response.rb +168 -0
  44. data/lib/adal/xml_namespaces.rb +64 -0
  45. data/samples/authorization_code_example/README.md +10 -0
  46. data/samples/authorization_code_example/web_app.rb +139 -0
  47. data/samples/client_assertion_certificate_example/README.md +42 -0
  48. data/samples/client_assertion_certificate_example/app.rb +55 -0
  49. data/samples/on_behalf_of_example/README.md +35 -0
  50. data/samples/on_behalf_of_example/native_app.rb +52 -0
  51. data/samples/on_behalf_of_example/web_api.rb +71 -0
  52. data/samples/user_credentials_example/README.md +7 -0
  53. data/samples/user_credentials_example/app.rb +52 -0
  54. data/spec/adal/authentication_context_spec.rb +186 -0
  55. data/spec/adal/authentication_parameters_spec.rb +107 -0
  56. data/spec/adal/authority_spec.rb +122 -0
  57. data/spec/adal/cache_driver_spec.rb +191 -0
  58. data/spec/adal/cached_token_response_spec.rb +148 -0
  59. data/spec/adal/client_assertion_certificate_spec.rb +113 -0
  60. data/spec/adal/client_assertion_spec.rb +38 -0
  61. data/spec/adal/core_ext/hash_spec.rb +47 -0
  62. data/spec/adal/logging_spec.rb +48 -0
  63. data/spec/adal/memory_cache_spec.rb +107 -0
  64. data/spec/adal/mex_request_spec.rb +57 -0
  65. data/spec/adal/mex_response_spec.rb +143 -0
  66. data/spec/adal/self_signed_jwt_factory_spec.rb +63 -0
  67. data/spec/adal/token_request_spec.rb +150 -0
  68. data/spec/adal/token_response_spec.rb +102 -0
  69. data/spec/adal/user_credential_spec.rb +125 -0
  70. data/spec/adal/user_identifier_spec.rb +115 -0
  71. data/spec/adal/wstrust_request_spec.rb +51 -0
  72. data/spec/adal/wstrust_response_spec.rb +152 -0
  73. data/spec/fixtures/mex/insecureaddress.xml +924 -0
  74. data/spec/fixtures/mex/invalid_namespaces.xml +916 -0
  75. data/spec/fixtures/mex/malformed.xml +914 -0
  76. data/spec/fixtures/mex/microsoft.xml +916 -0
  77. data/spec/fixtures/mex/multiple_endpoints.xml +922 -0
  78. data/spec/fixtures/mex/no_matching_bindings.xml +916 -0
  79. data/spec/fixtures/mex/no_username_token_policies.xml +914 -0
  80. data/spec/fixtures/mex/no_wstrust_endpoints.xml +838 -0
  81. data/spec/fixtures/mex/only_13.xml +842 -0
  82. data/spec/fixtures/mex/only_2005.xml +842 -0
  83. data/spec/fixtures/oauth/error.json +1 -0
  84. data/spec/fixtures/oauth/success.json +1 -0
  85. data/spec/fixtures/oauth/success_with_id_token.json +1 -0
  86. data/spec/fixtures/wstrust/error.xml +24 -0
  87. data/spec/fixtures/wstrust/invalid_namespaces.xml +136 -0
  88. data/spec/fixtures/wstrust/missing_security_tokens.xml +90 -0
  89. data/spec/fixtures/wstrust/success.xml +136 -0
  90. data/spec/fixtures/wstrust/token.xml +1 -0
  91. data/spec/fixtures/wstrust/too_many_security_tokens.xml +219 -0
  92. data/spec/fixtures/wstrust/unrecognized_token_type.xml +136 -0
  93. data/spec/fixtures/wstrust/wstrust.13.xml +1 -0
  94. data/spec/fixtures/wstrust/wstrust.2005.xml +89 -0
  95. data/spec/spec_helper.rb +53 -0
  96. data/spec/support/fake_data.rb +40 -0
  97. data/spec/support/fake_token_endpoint.rb +108 -0
  98. metadata +265 -0
@@ -0,0 +1,38 @@
1
+ #-------------------------------------------------------------------------------
2
+ # Copyright (c) 2015 Micorosft Corporation
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ # of this software and associated documentation files (the "Software"), to deal
6
+ # in the Software without restriction, including without limitation the rights
7
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ # copies of the Software, and to permit persons to whom the Software is
9
+ # furnished to do so, subject to the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be included in
12
+ # all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20
+ # THE SOFTWARE.
21
+ #-------------------------------------------------------------------------------
22
+
23
+ module ADAL
24
+ # A cache implementation that holds no values and ignores all method calls.
25
+ class NoopCache
26
+ # Swallows any number of parameters and returns nil.
27
+ def noop(*); end
28
+
29
+ alias_method :add, :noop
30
+ alias_method :add_many, :noop
31
+ alias_method :remove, :noop
32
+ alias_method :remove_many, :noop
33
+
34
+ def find(*)
35
+ []
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,76 @@
1
+ #-------------------------------------------------------------------------------
2
+ # Copyright (c) 2015 Micorosft Corporation
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ # of this software and associated documentation files (the "Software"), to deal
6
+ # in the Software without restriction, including without limitation the rights
7
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ # copies of the Software, and to permit persons to whom the Software is
9
+ # furnished to do so, subject to the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be included in
12
+ # all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20
+ # THE SOFTWARE.
21
+ #-------------------------------------------------------------------------------
22
+
23
+ require_relative './logging'
24
+ require_relative './request_parameters'
25
+ require_relative './util'
26
+
27
+ require 'net/http'
28
+ require 'uri'
29
+
30
+ module ADAL
31
+ # A request that can be made to an authentication or token server.
32
+ class OAuthRequest
33
+ include RequestParameters
34
+ include Util
35
+
36
+ DEFAULT_CONTENT_TYPE = 'application/x-www-form-urlencoded'
37
+ DEFAULT_ENCODING = 'utf8'
38
+ SSL_SCHEME = 'https'
39
+
40
+ def initialize(endpoint, params)
41
+ @endpoint_uri = URI.parse(endpoint.to_s)
42
+ @params = params
43
+ end
44
+
45
+ def params
46
+ default_parameters.merge(@params)
47
+ end
48
+
49
+ ##
50
+ # Requests and waits for a token from the endpoint.
51
+ # @return TokenResponse
52
+ def execute
53
+ request = Net::HTTP::Post.new(@endpoint_uri.path)
54
+ add_headers(request)
55
+ request.body = URI.encode_www_form(string_hash(params))
56
+ TokenResponse.parse(http(@endpoint_uri).request(request).body)
57
+ end
58
+
59
+ private
60
+
61
+ ##
62
+ # Adds the necessary OAuth headers.
63
+ #
64
+ # @param Net::HTTPGenericRequest
65
+ def add_headers(request)
66
+ return if Logging.correlation_id.nil?
67
+ request.add_field(CLIENT_REQUEST_ID.to_s, Logging.correlation_id)
68
+ request.add_field(CLIENT_RETURN_CLIENT_REQUEST_ID.to_s, true)
69
+ end
70
+
71
+ def default_parameters
72
+ { encoding: DEFAULT_ENCODING,
73
+ AAD_API_VERSION => '1.0' }
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,48 @@
1
+ #-------------------------------------------------------------------------------
2
+ # Copyright (c) 2015 Micorosft Corporation
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ # of this software and associated documentation files (the "Software"), to deal
6
+ # in the Software without restriction, including without limitation the rights
7
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ # copies of the Software, and to permit persons to whom the Software is
9
+ # furnished to do so, subject to the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be included in
12
+ # all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20
+ # THE SOFTWARE.
21
+ #-------------------------------------------------------------------------------
22
+
23
+ module ADAL
24
+ # Names of parameters in OAuth requests. This module can be included in any
25
+ # class to reference the parameters instead of referring to them as strings
26
+ # or symbols
27
+ module RequestParameters
28
+ AAD_API_VERSION = 'api-version'.to_sym
29
+ ASSERTION = 'assertion'.to_sym
30
+ CLIENT_ASSERTION = 'client_assertion'.to_sym
31
+ CLIENT_ASSERTION_TYPE = 'client_assertion_type'.to_sym
32
+ CLIENT_ID = 'client_id'.to_sym
33
+ CLIENT_REQUEST_ID = 'client-request-id'.to_sym
34
+ CLIENT_RETURN_CLIENT_REQUEST_ID = 'client-return-client-request-id'.to_sym
35
+ CLIENT_SECRET = 'client_secret'.to_sym
36
+ CODE = 'code'.to_sym
37
+ FORM_POST = 'form_post'.to_sym
38
+ GRANT_TYPE = 'grant_type'.to_sym
39
+ PASSWORD = 'password'.to_sym
40
+ REDIRECT_URI = 'redirect_uri'.to_sym
41
+ REFRESH_TOKEN = 'refresh_token'.to_sym
42
+ RESOURCE = 'resource'.to_sym
43
+ SCOPE = 'scope'.to_sym
44
+ UNIQUE_ID = 'unique_id'.to_sym
45
+ USER_INFO = 'user_info'.to_sym
46
+ USERNAME = 'username'.to_sym
47
+ end
48
+ end
@@ -0,0 +1,96 @@
1
+ #-------------------------------------------------------------------------------
2
+ # Copyright (c) 2015 Micorosft Corporation
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ # of this software and associated documentation files (the "Software"), to deal
6
+ # in the Software without restriction, including without limitation the rights
7
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ # copies of the Software, and to permit persons to whom the Software is
9
+ # furnished to do so, subject to the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be included in
12
+ # all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20
+ # THE SOFTWARE.
21
+ #-------------------------------------------------------------------------------
22
+
23
+ require_relative './jwt_parameters'
24
+ require_relative './logging'
25
+
26
+ require 'jwt'
27
+ require 'openssl'
28
+ require 'securerandom'
29
+
30
+ module ADAL
31
+ # Converts client certificates into self signed JWTs.
32
+ class SelfSignedJwtFactory
33
+ include JwtParameters
34
+ include Logging
35
+
36
+ ##
37
+ # Constructs a new SelfSignedJwtFactory.
38
+ #
39
+ # @param String client_id
40
+ # The client id of the calling application.
41
+ # @param String token_endpoint
42
+ # The token endpoint that will accept the certificate.
43
+ def initialize(client_id, token_endpoint)
44
+ @client_id = client_id
45
+ @token_endpoint = token_endpoint
46
+ end
47
+
48
+ ##
49
+ # Creates a JWT from a client certificate and signs it with a private key.
50
+ #
51
+ # @param OpenSSL::X509::Certificate certificate
52
+ # The certifcate object to be converted to a JWT and signed for use
53
+ # in an authentication flow.
54
+ # @param OpenSSL::PKey::RSA private_key
55
+ # The private key used to sign the certificate.
56
+ # @return String
57
+ def create_and_sign_jwt(certificate, private_key)
58
+ JWT.encode(payload, private_key, RS256, header(certificate))
59
+ end
60
+
61
+ private
62
+
63
+ # The JWT header for a certificate to be encoded.
64
+ def header(certificate)
65
+ x5t = thumbprint(certificate)
66
+ logger.verbose("Creating self signed JWT header with thumbprint: #{x5t}.")
67
+ { TYPE => TYPE_JWT,
68
+ ALGORITHM => RS256,
69
+ THUMBPRINT => x5t }
70
+ end
71
+
72
+ # The JWT payload.
73
+ def payload
74
+ now = Time.now - 1
75
+ expires = now + 60 * SELF_SIGNED_JWT_LIFETIME
76
+ logger.verbose("Creating self signed JWT payload. Expires: #{expires}. " \
77
+ "NotBefore: #{now}.")
78
+ { AUDIENCE => @token_endpoint,
79
+ ISSUER => @client_id,
80
+ SUBJECT => @client_id,
81
+ NOT_BEFORE => now.to_i,
82
+ EXPIRES_ON => expires.to_i,
83
+ JWT_ID => SecureRandom.uuid }
84
+ end
85
+
86
+ ##
87
+ # Base 64 encoded thumbprint AKA fingerprint AKA SHA1 hash of the
88
+ # DER representation of the cert.
89
+ #
90
+ # @param OpenSSL::X509::Certificate certificate
91
+ # @return String
92
+ def thumbprint(certificate)
93
+ OpenSSL::Digest::SHA1.new(certificate.to_der).base64digest
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,35 @@
1
+ <?xml version="1.0"?>
2
+ <s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:a="http://www.w3.org/2005/08/addressing" xmlns:u="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
3
+ <s:Header>
4
+ <a:Action s:mustUnderstand="1">http://docs.oasis-open.org/ws-sx/ws-trust/200512/RST/Issue</a:Action>
5
+ <a:messageID>urn:uuid:<%= message_id %></a:messageID>
6
+ <a:ReplyTo>
7
+ <a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address>
8
+ </a:ReplyTo>
9
+ <a:To s:mustUnderstand="1"><%= @endpoint %></a:To>
10
+ <o:Security xmlns:o="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" s:mustUnderstand="1">
11
+ <u:Timestamp u:Id="_0">
12
+ <u:Created><%= created.utc.iso8601 %></u:Created>
13
+ <u:Expires><%= expires.utc.iso8601 %></u:Expires>
14
+ </u:Timestamp>
15
+ <o:UsernameToken u:Id="ADALUsernameToken">
16
+ <o:Username><%= username.encode(xml: :text) %></o:Username>
17
+ <o:Password><%= password.encode(xml: :text) %></o:Password>
18
+ </o:UsernameToken>
19
+ </o:Security>
20
+ </s:Header>
21
+ <s:Body>
22
+ <trust:RequestSecurityToken xmlns:trust="http://docs.oasis-open.org/ws-sx/ws-trust/200512">
23
+ <wsp:AppliesTo xmlns:wsp="http://schemas.xmlsoap.org/ws/2004/09/policy">
24
+ <a:EndpointReference>
25
+ <a:Address><%= @applies_to %></a:Address>
26
+ </a:EndpointReference>
27
+ </wsp:AppliesTo>
28
+ <trust:KeyType>http://docs.oasis-open.org/ws-sx/ws-trust/200512/Bearer</trust:KeyType>
29
+ <trust:RequestType>http://docs.oasis-open.org/ws-sx/ws-trust/200512/Issue</trust:RequestType>
30
+ </trust:RequestSecurityToken>
31
+ </s:Body>
32
+ </s:Envelope>
33
+
34
+
35
+
@@ -0,0 +1,32 @@
1
+ <?xml version="1.0"?>
2
+ <s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:a="http://www.w3.org/2005/08/addressing" xmlns:u="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
3
+ <s:Header>
4
+ <a:Action s:mustUnderstand="1">http://schemas.xmlsoap.org/ws/2005/02/trust/RST/Issue</a:Action>
5
+ <a:messageID>urn:uuid:<%= message_id %></a:messageID>
6
+ <a:ReplyTo>
7
+ <a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address>
8
+ </a:ReplyTo>
9
+ <a:To s:mustUnderstand="1"><%= @endpoint %></a:To>
10
+ <o:Security xmlns:o="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" s:mustUnderstand="1">
11
+ <u:Timestamp u:Id="_0">
12
+ <u:Created><%= created.utc.iso8601 %></u:Created>
13
+ <u:Expires><%= expires.utc.iso8601 %></u:Expires>
14
+ </u:Timestamp>
15
+ <o:UsernameToken u:Id="ADALUsernameToken">
16
+ <o:Username><%= username.encode(xml: :text) %></o:Username>
17
+ <o:Password><%= password.encode(xml: :text) %></o:Password>
18
+ </o:UsernameToken>
19
+ </o:Security>
20
+ </s:Header>
21
+ <s:Body>
22
+ <trust:RequestSecurityToken xmlns:trust="http://schemas.xmlsoap.org/ws/2005/02/trust">
23
+ <wsp:AppliesTo xmlns:wsp="http://schemas.xmlsoap.org/ws/2004/09/policy">
24
+ <a:EndpointReference>
25
+ <a:Address><%= @applies_to %></a:Address>
26
+ </a:EndpointReference>
27
+ </wsp:AppliesTo>
28
+ <trust:KeyType>http://schemas.xmlsoap.org/ws/2005/05/identity/NoProofKey</trust:KeyType>
29
+ <trust:RequestType>http://schemas.xmlsoap.org/ws/2005/02/trust/Issue</trust:RequestType>
30
+ </trust:RequestSecurityToken>
31
+ </s:Body>
32
+ </s:Envelope>
@@ -0,0 +1,231 @@
1
+ #-------------------------------------------------------------------------------
2
+ # Copyright (c) 2015 Micorosft Corporation
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ # of this software and associated documentation files (the "Software"), to deal
6
+ # in the Software without restriction, including without limitation the rights
7
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ # copies of the Software, and to permit persons to whom the Software is
9
+ # furnished to do so, subject to the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be included in
12
+ # all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20
+ # THE SOFTWARE.
21
+ #-------------------------------------------------------------------------------
22
+
23
+ require_relative './cache_driver'
24
+ require_relative './logging'
25
+ require_relative './noop_cache'
26
+ require_relative './oauth_request'
27
+ require_relative './request_parameters'
28
+
29
+ require 'openssl'
30
+
31
+ module ADAL
32
+ # A request for a token that may be fulfilled by a cache or an OAuthRequest
33
+ # to a token endpoint.
34
+ class TokenRequest
35
+ include Logging
36
+ include RequestParameters
37
+
38
+ # An error that signifies an attempt to perform OAuth with a UserIdentifier.
39
+ # UserIdentifiers can only be used to retrieve access tokens from the cache,
40
+ # so if no matching cache token is found, this error is thrown.
41
+ class UserCredentialError < StandardError; end
42
+
43
+ # All accepted grant types. This module can be mixed-in to other classes
44
+ # that require them.
45
+ module GrantType
46
+ AUTHORIZATION_CODE = 'authorization_code'
47
+ CLIENT_CREDENTIALS = 'client_credentials'
48
+ JWT_BEARER = 'urn:ietf:params:oauth:grant-type:jwt-bearer'
49
+ PASSWORD = 'password'
50
+ REFRESH_TOKEN = 'refresh_token'
51
+ SAML1 = 'urn:ietf:params:oauth:grant-type:saml1_1-bearer'
52
+ SAML2 = 'urn:ietf:params:oauth:grant-type:saml2-bearer'
53
+ end
54
+
55
+ ##
56
+ # Constructs a TokenRequest.
57
+ #
58
+ # @param [Authority] authority
59
+ # The Authority providing authorization and token endpoints.
60
+ # @param ClientCredential|ClientAssertion|ClientAssertionCertificate
61
+ # Used to identify the client. Provides a request_parameters method
62
+ # that yields the relevant client credential parameters.
63
+ # @option [TokenCache] token_cache
64
+ # The cache implementation to store tokens. A NoopCache that stores no
65
+ # tokens will be used by default.
66
+ def initialize(authority, client, token_cache = NoopCache.new)
67
+ @authority = authority
68
+ @cache_driver = CacheDriver.new(authority, client, token_cache)
69
+ @client = client
70
+ @token_cache = token_cache
71
+ end
72
+
73
+ public
74
+
75
+ ##
76
+ # Gets a token based solely on the clients credentials that were used to
77
+ # initialize the token request.
78
+ #
79
+ # @param String resource
80
+ # The resource for which the requested access token will provide access.
81
+ # @return TokenResponse
82
+ def get_for_client(resource)
83
+ logger.verbose("TokenRequest getting token for client for #{resource}.")
84
+ request(GRANT_TYPE => GrantType::CLIENT_CREDENTIALS,
85
+ RESOURCE => resource)
86
+ end
87
+
88
+ ##
89
+ # Gets a token based on a previously acquired authentication code.
90
+ #
91
+ # @param String auth_code
92
+ # An authentication code that was previously acquired from an
93
+ # authentication endpoint.
94
+ # @param String redirect_uri
95
+ # The redirect uri that was passed to the authentication endpoint when the
96
+ # auth code was acquired.
97
+ # @optional String resource
98
+ # The resource for which the requested access token will provide access.
99
+ # @return TokenResponse
100
+ def get_with_authorization_code(auth_code, redirect_uri, resource = nil)
101
+ logger.verbose('TokenRequest getting token with authorization code ' \
102
+ "#{auth_code}, redirect_uri #{redirect_uri} and " \
103
+ "resource #{resource}.")
104
+ request(CODE => auth_code,
105
+ GRANT_TYPE => GrantType::AUTHORIZATION_CODE,
106
+ REDIRECT_URI => URI.parse(redirect_uri.to_s),
107
+ RESOURCE => resource)
108
+ end
109
+
110
+ ##
111
+ # Gets a token based on a previously acquired refresh token.
112
+ #
113
+ # @param String refresh_token
114
+ # The refresh token that was previously acquired from a token response.
115
+ # @optional String resource
116
+ # The resource for which the requested access token will provide access.
117
+ # @return TokenResponse
118
+ def get_with_refresh_token(refresh_token, resource = nil)
119
+ logger.verbose('TokenRequest getting token with refresh token digest ' \
120
+ "#{Digest::SHA256.hexdigest refresh_token} and resource " \
121
+ "#{resource}.")
122
+ request_no_cache(GRANT_TYPE => GrantType::REFRESH_TOKEN,
123
+ REFRESH_TOKEN => refresh_token,
124
+ RESOURCE => resource)
125
+ end
126
+
127
+ ##
128
+ # Gets a token based on possessing the users credentials.
129
+ #
130
+ # @param UserCredential|UserIdentifier user_cred
131
+ # Something that can be used to verify the user. Typically a username
132
+ # and password. If it is a UserIdentifier, only the cache will be checked.
133
+ # If a matching token is not there, it will fail.
134
+ # @optional String resource
135
+ # The resource for which the requested access token will provide access.
136
+ # @return TokenResponse
137
+ def get_with_user_credential(user_cred, resource = nil)
138
+ logger.verbose('TokenRequest getting token with user credential ' \
139
+ "#{user_cred} and resource #{resource}.")
140
+ oauth = if user_cred.is_a? UserIdentifier
141
+ lambda do
142
+ fail UserCredentialError,
143
+ 'UserIdentifier can only be used once there is a ' \
144
+ 'matching token in the cache.'
145
+ end
146
+ end || -> {}
147
+ request(user_cred.request_params.merge(RESOURCE => resource), &oauth)
148
+ end
149
+
150
+ private
151
+
152
+ ##
153
+ # The OAuth parameters that are specific to the client for which tokens will
154
+ # be requested.
155
+ #
156
+ # @return Hash
157
+ def client_params
158
+ @client.request_params
159
+ end
160
+
161
+ ##
162
+ # Attempts to fulfill a token request, first via the token cache and then
163
+ # through OAuth.
164
+ #
165
+ # @param Hash params
166
+ # Any additional request parameters that should be used.
167
+ # @return TokenResponse
168
+ def request(params, &block)
169
+ cached_token = check_cache(request_params(params))
170
+ return cached_token if cached_token
171
+ cache_response(request_no_cache(request_params(params), &block))
172
+ end
173
+
174
+ ##
175
+ # Executes an OAuth request based on the params and returns it.
176
+ #
177
+ # @param Hash params
178
+ # Any additional request parameters that should be used.
179
+ # @return TokenResponse
180
+ def request_no_cache(params)
181
+ yield if block_given?
182
+ oauth_request(request_params(params)).execute
183
+ end
184
+
185
+ ##
186
+ # Adds client params to additional params. If there is a conflict, the value
187
+ # from additional_params is used. It can be called multiple times, because
188
+ # request_params(request_params(x)) == request_params(x).
189
+ #
190
+ # @param Hash
191
+ # @return Hash
192
+ def request_params(additional_params)
193
+ client_params.merge(additional_params).select { |_, v| !v.nil? }
194
+ end
195
+
196
+ ##
197
+ # Helper method to chain OAuthRequest and cache operation.
198
+ #
199
+ # @param TokenResponse
200
+ # The token response to cache.
201
+ # @return TokenResponse
202
+ def cache_response(token_response)
203
+ @cache_driver.add(token_response)
204
+ token_response
205
+ end
206
+
207
+ ##
208
+ # Attempts to fulfill the request from @token_cache.
209
+ #
210
+ # @return TokenResponse
211
+ # If the cache contains a valid response it wil be returned as a
212
+ # SuccessResponse. Otherwise returns nil.
213
+ def check_cache(params)
214
+ logger.verbose("TokenRequest checking cache #{@token_cache} for token.")
215
+ result = @cache_driver.find(params)
216
+ logger.info("#{result ? 'Found' : 'Did not find'} token in cache.")
217
+ result
218
+ end
219
+
220
+ ##
221
+ # Constructs an OAuthRequest from the TokenRequest instance.
222
+ #
223
+ # @param Hash params
224
+ # The OAuth parameters specific to the TokenRequest instance.
225
+ # @return OAuthRequest
226
+ def oauth_request(params)
227
+ logger.verbose('Resorting to OAuth to fulfill token request.')
228
+ OAuthRequest.new(@authority.token_endpoint, params)
229
+ end
230
+ end
231
+ end