55import string
66import warnings
77import hashlib
8+ import logging
89
910from . import oauth2
1011
12+
13+ logger = logging .getLogger (__name__ )
14+
1115def decode_part (raw , encoding = "utf-8" ):
1216 """Decode a part of the JWT.
1317
@@ -32,6 +36,45 @@ def decode_part(raw, encoding="utf-8"):
3236
3337base64decode = decode_part # Obsolete. For backward compatibility only.
3438
39+ def _epoch_to_local (epoch ):
40+ return time .strftime ("%Y-%m-%d %H:%M:%S" , time .localtime (epoch ))
41+
42+ class IdTokenError (RuntimeError ): # We waised RuntimeError before, so keep it
43+ """In unlikely event of an ID token is malformed, this exception will be raised."""
44+ def __init__ (self , reason , now , claims ):
45+ super (IdTokenError , self ).__init__ (
46+ "%s Current epoch = %s. The id_token was approximately: %s" % (
47+ reason , _epoch_to_local (now ), json .dumps (dict (
48+ claims ,
49+ iat = _epoch_to_local (claims ["iat" ]) if claims .get ("iat" ) else None ,
50+ exp = _epoch_to_local (claims ["exp" ]) if claims .get ("exp" ) else None ,
51+ ), indent = 2 )))
52+
53+ class _IdTokenTimeError (IdTokenError ): # This is not intended to be raised and caught
54+ _SUGGESTION = "Make sure your computer's time and time zone are both correct."
55+ def __init__ (self , reason , now , claims ):
56+ super (_IdTokenTimeError , self ).__init__ (reason + " " + self ._SUGGESTION , now , claims )
57+ def log (self ):
58+ # Influenced by JWT specs https://tools.ietf.org/html/rfc7519#section-4.1.5
59+ # and OIDC specs https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
60+ # We used to raise this error, but now we just log it as warning, because:
61+ # 1. If it is caused by incorrect local machine time,
62+ # then the token(s) are still correct and probably functioning,
63+ # so, there is no point to error out.
64+ # 2. If it is caused by incorrect IdP time, then it is IdP's fault,
65+ # There is not much a client can do, so, we might as well return the token(s)
66+ # and let downstream components to decide what to do.
67+ logger .warning (str (self ))
68+
69+ class IdTokenIssuerError (IdTokenError ):
70+ pass
71+
72+ class IdTokenAudienceError (IdTokenError ):
73+ pass
74+
75+ class IdTokenNonceError (IdTokenError ):
76+ pass
77+
3578def decode_id_token (id_token , client_id = None , issuer = None , nonce = None , now = None ):
3679 """Decodes and validates an id_token and returns its claims as a dictionary.
3780
@@ -41,41 +84,52 @@ def decode_id_token(id_token, client_id=None, issuer=None, nonce=None, now=None)
4184 `maybe more <https://openid.net/specs/openid-connect-core-1_0.html#Claims>`_
4285 """
4386 decoded = json .loads (decode_part (id_token .split ('.' )[1 ]))
44- err = None # https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
87+ # Based on https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
4588 _now = int (now or time .time ())
4689 skew = 120 # 2 minutes
47- TIME_SUGGESTION = "Make sure your computer's time and time zone are both correct."
90+
4891 if _now + skew < decoded .get ("nbf" , _now - 1 ): # nbf is optional per JWT specs
4992 # This is not an ID token validation, but a JWT validation
5093 # https://tools.ietf.org/html/rfc7519#section-4.1.5
51- err = "0. The ID token is not yet valid. " + TIME_SUGGESTION
94+ _IdTokenTimeError ("0. The ID token is not yet valid." , _now , decoded ).log ()
95+
5296 if issuer and issuer != decoded ["iss" ]:
5397 # https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse
54- err = ('2. The Issuer Identifier for the OpenID Provider, "%s", '
98+ raise IdTokenIssuerError (
99+ '2. The Issuer Identifier for the OpenID Provider, "%s", '
55100 "(which is typically obtained during Discovery), "
56- "MUST exactly match the value of the iss (issuer) Claim." ) % issuer
101+ "MUST exactly match the value of the iss (issuer) Claim." % issuer ,
102+ _now ,
103+ decoded )
104+
57105 if client_id :
58106 valid_aud = client_id in decoded ["aud" ] if isinstance (
59107 decoded ["aud" ], list ) else client_id == decoded ["aud" ]
60108 if not valid_aud :
61- err = (
109+ raise IdTokenAudienceError (
62110 "3. The aud (audience) claim must contain this client's client_id "
63111 '"%s", case-sensitively. Was your client_id in wrong casing?'
64112 # Some IdP accepts wrong casing request but issues right casing IDT
65- ) % client_id
113+ % client_id ,
114+ _now ,
115+ decoded )
116+
66117 # Per specs:
67118 # 6. If the ID Token is received via direct communication between
68119 # the Client and the Token Endpoint (which it is during _obtain_token()),
69120 # the TLS server validation MAY be used to validate the issuer
70121 # in place of checking the token signature.
122+
71123 if _now - skew > decoded ["exp" ]:
72- err = "9. The ID token already expires. " + TIME_SUGGESTION
124+ _IdTokenTimeError ("9. The ID token already expires." , _now , decoded ).log ()
125+
73126 if nonce and nonce != decoded .get ("nonce" ):
74- err = ("11. Nonce must be the same value "
75- "as the one that was sent in the Authentication Request." )
76- if err :
77- raise RuntimeError ("%s Current epoch = %s. The id_token was: %s" % (
78- err , _now , json .dumps (decoded , indent = 2 )))
127+ raise IdTokenNonceError (
128+ "11. Nonce must be the same value "
129+ "as the one that was sent in the Authentication Request." ,
130+ _now ,
131+ decoded )
132+
79133 return decoded
80134
81135
0 commit comments