1import base64
2import hashlib
3import logging
4import time
5from json import loads
6
7from oauthlib.oauth2.rfc6749.errors import (
8 ConsentRequired, InvalidRequestError, LoginRequired,
9)
10
11log = logging.getLogger(__name__)
12
13
14class GrantTypeBase:
15
16 # Just proxy the majority of method calls through to the
17 # proxy_target grant type handler, which will usually be either
18 # the standard OAuth2 AuthCode or Implicit grant types.
19 def __getattr__(self, attr):
20 return getattr(self.proxy_target, attr)
21
22 def __setattr__(self, attr, value):
23 proxied_attrs = {'refresh_token', 'response_types'}
24 if attr in proxied_attrs:
25 setattr(self.proxy_target, attr, value)
26 else:
27 super(OpenIDConnectBase, self).__setattr__(attr, value)
28
29 def validate_authorization_request(self, request):
30 """Validates the OpenID Connect authorization request parameters.
31
32 :returns: (list of scopes, dict of request info)
33 """
34 return self.proxy_target.validate_authorization_request(request)
35
36 def _inflate_claims(self, request):
37 # this may be called multiple times in a single request so make sure we only de-serialize the claims once
38 if request.claims and not isinstance(request.claims, dict):
39 # specific claims are requested during the Authorization Request and may be requested for inclusion
40 # in either the id_token or the UserInfo endpoint response
41 # see http://openid.net/specs/openid-connect-core-1_0.html#ClaimsParameter
42 try:
43 request.claims = loads(request.claims)
44 except Exception as ex:
45 raise InvalidRequestError(description="Malformed claims parameter",
46 uri="http://openid.net/specs/openid-connect-core-1_0.html#ClaimsParameter")
47
48 def id_token_hash(self, value, hashfunc=hashlib.sha256):
49 """
50 Its value is the base64url encoding of the left-most half of the
51 hash of the octets of the ASCII representation of the access_token
52 value, where the hash algorithm used is the hash algorithm used in
53 the alg Header Parameter of the ID Token's JOSE Header.
54
55 For instance, if the alg is RS256, hash the access_token value
56 with SHA-256, then take the left-most 128 bits and
57 base64url-encode them.
58 For instance, if the alg is HS512, hash the code value with
59 SHA-512, then take the left-most 256 bits and base64url-encode
60 them. The c_hash value is a case-sensitive string.
61
62 Example of hash from OIDC specification (bound to a JWS using RS256):
63
64 code:
65 Qcb0Orv1zh30vL1MPRsbm-diHiMwcLyZvn1arpZv-Jxf_11jnpEX3Tgfvk
66
67 c_hash:
68 LDktKdoQak3Pk0cnXxCltA
69 """
70 digest = hashfunc(value.encode()).digest()
71 left_most = len(digest) // 2
72 return base64.urlsafe_b64encode(digest[:left_most]).decode().rstrip("=")
73
74 def add_id_token(self, token, token_handler, request, nonce=None):
75 """
76 Construct an initial version of id_token, and let the
77 request_validator sign or encrypt it.
78
79 The initial version can contain the fields below, accordingly
80 to the spec:
81 - aud
82 - iat
83 - nonce
84 - at_hash
85 - c_hash
86 """
87 # Treat it as normal OAuth 2 auth code request if openid is not present
88 if not request.scopes or 'openid' not in request.scopes:
89 return token
90
91 # Only add an id token on auth/token step if asked for.
92 if request.response_type and 'id_token' not in request.response_type:
93 return token
94
95 # Implementation mint its own id_token without help.
96 id_token = self.request_validator.get_id_token(token, token_handler, request)
97 if id_token:
98 token['id_token'] = id_token
99 return token
100
101 # Fallback for asking some help from oauthlib framework.
102 # Start with technicals fields bound to the specification.
103 id_token = {}
104 id_token['aud'] = request.client_id
105 id_token['iat'] = int(time.time())
106
107 # nonce is REQUIRED when response_type value is:
108 # - id_token token (Implicit)
109 # - id_token (Implicit)
110 # - code id_token (Hybrid)
111 # - code id_token token (Hybrid)
112 #
113 # nonce is OPTIONAL when response_type value is:
114 # - code (Authorization Code)
115 # - code token (Hybrid)
116 if nonce is not None:
117 id_token["nonce"] = nonce
118
119 # at_hash is REQUIRED when response_type value is:
120 # - id_token token (Implicit)
121 # - code id_token token (Hybrid)
122 #
123 # at_hash is OPTIONAL when:
124 # - code (Authorization code)
125 # - code id_token (Hybrid)
126 # - code token (Hybrid)
127 #
128 # at_hash MAY NOT be used when:
129 # - id_token (Implicit)
130 if "access_token" in token:
131 id_token["at_hash"] = self.id_token_hash(token["access_token"])
132
133 # c_hash is REQUIRED when response_type value is:
134 # - code id_token (Hybrid)
135 # - code id_token token (Hybrid)
136 #
137 # c_hash is OPTIONAL for others.
138 if "code" in token:
139 id_token["c_hash"] = self.id_token_hash(token["code"])
140
141 # Call request_validator to complete/sign/encrypt id_token
142 token['id_token'] = self.request_validator.finalize_id_token(id_token, token, token_handler, request)
143
144 return token
145
146 def openid_authorization_validator(self, request):
147 """Perform OpenID Connect specific authorization request validation.
148
149 nonce
150 OPTIONAL. String value used to associate a Client session with
151 an ID Token, and to mitigate replay attacks. The value is
152 passed through unmodified from the Authentication Request to
153 the ID Token. Sufficient entropy MUST be present in the nonce
154 values used to prevent attackers from guessing values
155
156 display
157 OPTIONAL. ASCII string value that specifies how the
158 Authorization Server displays the authentication and consent
159 user interface pages to the End-User. The defined values are:
160
161 page - The Authorization Server SHOULD display the
162 authentication and consent UI consistent with a full User
163 Agent page view. If the display parameter is not specified,
164 this is the default display mode.
165
166 popup - The Authorization Server SHOULD display the
167 authentication and consent UI consistent with a popup User
168 Agent window. The popup User Agent window should be of an
169 appropriate size for a login-focused dialog and should not
170 obscure the entire window that it is popping up over.
171
172 touch - The Authorization Server SHOULD display the
173 authentication and consent UI consistent with a device that
174 leverages a touch interface.
175
176 wap - The Authorization Server SHOULD display the
177 authentication and consent UI consistent with a "feature
178 phone" type display.
179
180 The Authorization Server MAY also attempt to detect the
181 capabilities of the User Agent and present an appropriate
182 display.
183
184 prompt
185 OPTIONAL. Space delimited, case sensitive list of ASCII string
186 values that specifies whether the Authorization Server prompts
187 the End-User for reauthentication and consent. The defined
188 values are:
189
190 none - The Authorization Server MUST NOT display any
191 authentication or consent user interface pages. An error is
192 returned if an End-User is not already authenticated or the
193 Client does not have pre-configured consent for the
194 requested Claims or does not fulfill other conditions for
195 processing the request. The error code will typically be
196 login_required, interaction_required, or another code
197 defined in Section 3.1.2.6. This can be used as a method to
198 check for existing authentication and/or consent.
199
200 login - The Authorization Server SHOULD prompt the End-User
201 for reauthentication. If it cannot reauthenticate the
202 End-User, it MUST return an error, typically
203 login_required.
204
205 consent - The Authorization Server SHOULD prompt the
206 End-User for consent before returning information to the
207 Client. If it cannot obtain consent, it MUST return an
208 error, typically consent_required.
209
210 select_account - The Authorization Server SHOULD prompt the
211 End-User to select a user account. This enables an End-User
212 who has multiple accounts at the Authorization Server to
213 select amongst the multiple accounts that they might have
214 current sessions for. If it cannot obtain an account
215 selection choice made by the End-User, it MUST return an
216 error, typically account_selection_required.
217
218 The prompt parameter can be used by the Client to make sure
219 that the End-User is still present for the current session or
220 to bring attention to the request. If this parameter contains
221 none with any other value, an error is returned.
222
223 max_age
224 OPTIONAL. Maximum Authentication Age. Specifies the allowable
225 elapsed time in seconds since the last time the End-User was
226 actively authenticated by the OP. If the elapsed time is
227 greater than this value, the OP MUST attempt to actively
228 re-authenticate the End-User. (The max_age request parameter
229 corresponds to the OpenID 2.0 PAPE [OpenID.PAPE] max_auth_age
230 request parameter.) When max_age is used, the ID Token returned
231 MUST include an auth_time Claim Value.
232
233 ui_locales
234 OPTIONAL. End-User's preferred languages and scripts for the
235 user interface, represented as a space-separated list of BCP47
236 [RFC5646] language tag values, ordered by preference. For
237 instance, the value "fr-CA fr en" represents a preference for
238 French as spoken in Canada, then French (without a region
239 designation), followed by English (without a region
240 designation). An error SHOULD NOT result if some or all of the
241 requested locales are not supported by the OpenID Provider.
242
243 id_token_hint
244 OPTIONAL. ID Token previously issued by the Authorization
245 Server being passed as a hint about the End-User's current or
246 past authenticated session with the Client. If the End-User
247 identified by the ID Token is logged in or is logged in by the
248 request, then the Authorization Server returns a positive
249 response; otherwise, it SHOULD return an error, such as
250 login_required. When possible, an id_token_hint SHOULD be
251 present when prompt=none is used and an invalid_request error
252 MAY be returned if it is not; however, the server SHOULD
253 respond successfully when possible, even if it is not present.
254 The Authorization Server need not be listed as an audience of
255 the ID Token when it is used as an id_token_hint value. If the
256 ID Token received by the RP from the OP is encrypted, to use it
257 as an id_token_hint, the Client MUST decrypt the signed ID
258 Token contained within the encrypted ID Token. The Client MAY
259 re-encrypt the signed ID token to the Authentication Server
260 using a key that enables the server to decrypt the ID Token,
261 and use the re-encrypted ID token as the id_token_hint value.
262
263 login_hint
264 OPTIONAL. Hint to the Authorization Server about the login
265 identifier the End-User might use to log in (if necessary).
266 This hint can be used by an RP if it first asks the End-User
267 for their e-mail address (or other identifier) and then wants
268 to pass that value as a hint to the discovered authorization
269 service. It is RECOMMENDED that the hint value match the value
270 used for discovery. This value MAY also be a phone number in
271 the format specified for the phone_number Claim. The use of
272 this parameter is left to the OP's discretion.
273
274 acr_values
275 OPTIONAL. Requested Authentication Context Class Reference
276 values. Space-separated string that specifies the acr values
277 that the Authorization Server is being requested to use for
278 processing this Authentication Request, with the values
279 appearing in order of preference. The Authentication Context
280 Class satisfied by the authentication performed is returned as
281 the acr Claim Value, as specified in Section 2. The acr Claim
282 is requested as a Voluntary Claim by this parameter.
283 """
284
285 # Treat it as normal OAuth 2 auth code request if openid is not present
286 if not request.scopes or 'openid' not in request.scopes:
287 return {}
288
289 prompt = request.prompt if request.prompt else []
290 if hasattr(prompt, 'split'):
291 prompt = prompt.strip().split()
292 prompt = set(prompt)
293
294 if 'none' in prompt:
295
296 if len(prompt) > 1:
297 msg = "Prompt none is mutually exclusive with other values."
298 raise InvalidRequestError(request=request, description=msg)
299
300 if not self.request_validator.validate_silent_login(request):
301 raise LoginRequired(request=request)
302
303 if not self.request_validator.validate_silent_authorization(request):
304 raise ConsentRequired(request=request)
305
306 self._inflate_claims(request)
307
308 if not self.request_validator.validate_user_match(
309 request.id_token_hint, request.scopes, request.claims, request):
310 msg = "Session user does not match client supplied user."
311 raise LoginRequired(request=request, description=msg)
312
313 ui_locales = request.ui_locales if request.ui_locales else []
314 if hasattr(ui_locales, 'split'):
315 ui_locales = ui_locales.strip().split()
316
317 request_info = {
318 'display': request.display,
319 'nonce': request.nonce,
320 'prompt': prompt,
321 'ui_locales': ui_locales,
322 'id_token_hint': request.id_token_hint,
323 'login_hint': request.login_hint,
324 'claims': request.claims
325 }
326
327 return request_info
328
329
330OpenIDConnectBase = GrantTypeBase