1
+ """This OAuth2 client implementation aims to be spec-compliant, and generic."""
2
+ # OAuth2 spec https://tools.ietf.org/html/rfc6749
3
+
1
4
try :
2
5
from urllib .parse import urlencode , parse_qs
3
6
except ImportError :
7
10
import requests
8
11
9
12
10
- def validate_authorization (params , state = None ):
11
- """A thin helper to examine the authorization being redirected back"""
12
- if not isinstance (params , dict ):
13
- params = parse_qs (params )
14
- if params .get ('state' ) != state :
15
- raise ValueError ('state mismatch' )
16
- return params
17
-
18
-
19
13
class Client (object ):
20
- """This OAuth2 client implementation aims to be spec-compliant, and generic.
21
-
22
- https://tools.ietf.org/html/rfc6749
23
- """
14
+ # This low-level interface works. Yet you'll find those *Grant sub-classes
15
+ # more friendly to remind you what parameters are needed in each scenario.
24
16
def __init__ (
25
17
self , client_id ,
26
18
client_credential = None , # Only needed for Confidential Client
@@ -30,40 +22,15 @@ def __init__(
30
22
self .authorization_endpoint = authorization_endpoint
31
23
self .token_endpoint = token_endpoint
32
24
33
- def authorization_url (self ,
34
- response_type , # MUST be set to "code" or "token"
35
- redirect_uri = None ,
36
- scope = None ,
37
- state = None , # Recommended by the spec
38
- ** kwargs ):
39
- """To generate an authorization url, to be visited by resource owner.
40
-
41
- :param scope: It is a space-delimited, case-sensitive string.
42
- Some ID provider can accept empty string to represent default scope.
43
- """
44
- assert response_type and self .client_id
45
- sep = '&' if '?' in self .authorization_endpoint else '?'
46
- params = {
47
- 'client_id' : self .client_id ,
48
- 'response_type' : response_type ,
49
- 'redirect_uri' : redirect_uri ,
50
- 'scope' : scope ,
51
- 'state' : state ,
52
- }
25
+ def authorization_url (self , response_type , ** kwargs ):
26
+ params = {'client_id' : self .client_id , 'response_type' : response_type }
53
27
params .update (kwargs )
54
28
params = {k : v for k , v in params .items () if v is not None } # clean up
29
+ sep = '&' if '?' in self .authorization_endpoint else '?'
55
30
return "%s%s%s" % (self .authorization_endpoint , sep , urlencode (params ))
56
31
57
- def get_token (
58
- self , grant_type ,
59
- redirect_uri = None ,
60
- scope = None , # Not needed in Authorization Code Grant flow
61
- ** kwargs ):
62
- # Depending on your chosen grant flow, you may need 'code',
63
- # or 'username' & 'password' pairs, or none of them in the parameters
64
- data = {
65
- 'client_id' : self .client_id , 'grant_type' : grant_type ,
66
- 'scope' : scope }
32
+ def get_token (self , grant_type , ** kwargs ):
33
+ data = {'client_id' : self .client_id , 'grant_type' : grant_type }
67
34
data .update (kwargs )
68
35
# We don't need to clean up None values here, because requests lib will.
69
36
@@ -82,7 +49,7 @@ def get_token(
82
49
self .token_endpoint , headers = {'Accept' : 'application/json' },
83
50
data = data , auth = auth )
84
51
if resp .status_code >= 500 :
85
- resp .raise_for_status () # TODO: Will probably try to retry here
52
+ resp .raise_for_status () # TODO: Will probably retry here
86
53
# The spec (https://tools.ietf.org/html/rfc6749#section-5.2) says
87
54
# even an error response will be a valid json structure,
88
55
# so we simply return it here, without needing to invent an exception.
@@ -91,26 +58,51 @@ def get_token(
91
58
92
59
class AuthorizationCodeGrant (Client ):
93
60
94
- def authorization_url (self , ** kwargs ):
61
+ def authorization_url (
62
+ self , redirect_uri = None , scope = None , state = None , ** kwargs ):
63
+ """Generate an authorization url to be visited by resource owner.
64
+
65
+ :param response_type: MUST be set to "code" or "token".
66
+ :param scope: It is a space-delimited, case-sensitive string.
67
+ Some ID provider can accept empty string to represent default scope.
68
+ """
95
69
return super (AuthorizationCodeGrant , self ).authorization_url (
96
- 'code' , ** kwargs )
97
- # Later when you receive the redirected feedback,
70
+ 'code' , redirect_uri = redirect_uri , scope = scope , state = state ,
71
+ ** kwargs )
72
+ # Later when you receive the response at your redirect_uri,
98
73
# validate_authorization() may be handy to check the returned state.
99
74
100
- def get_token (self , code , ** kwargs ):
75
+ def get_token (self , code , redirect_uri = None , client_id = None , ** kwargs ):
76
+ """Get an access token.
77
+
78
+ See also https://tools.ietf.org/html/rfc6749#section-4.1.3
79
+
80
+ :param code: The authorization code received from authorization server.
81
+ :param redirect_uri:
82
+ Required, if the "redirect_uri" parameter was included in the
83
+ authorization request, and their values MUST be identical.
84
+ :param client_id: Required, if the client is not authenticating itself.
85
+ See https://tools.ietf.org/html/rfc6749#section-3.2.1
86
+ """
101
87
return super (AuthorizationCodeGrantFlow , self ).get_token (
102
- 'authorization_code' , code = code , ** kwargs )
88
+ 'authorization_code' , code = code ,
89
+ redirect_uri = redirect_uri , client_id = client_id , ** kwargs )
103
90
104
91
105
- class ImplicitGrant (Client ):
106
- """This class is only for illustrative purpose.
92
+ def validate_authorization (params , state = None ):
93
+ """A thin helper to examine the authorization being redirected back"""
94
+ if not isinstance (params , dict ):
95
+ params = parse_qs (params )
96
+ if params .get ('state' ) != state :
97
+ raise ValueError ('state mismatch' )
98
+ return params
107
99
108
- You probably won't implement your ImplicitGrant flow in Python.
109
- """
110
100
111
- def authorization_url (self , ** kwargs ):
112
- return super (ImplicitGrant , self ).authorization_url (
113
- 'token' , ** kwargs )
101
+ class ImplicitGrant (Client ):
102
+ # This class is only for illustrative purpose.
103
+ # You probably won't implement your ImplicitGrant flow in Python anyway.
104
+ def authorization_url (self , redirect_uri = None , scope = None , state = None ):
105
+ return super (ImplicitGrant , self ).authorization_url ('token' , ** locals ())
114
106
115
107
def get_token (self ):
116
108
raise NotImplemented ("Token is already issued during authorization" )
@@ -120,20 +112,20 @@ class ResourceOwnerPasswordCredentialsGrant(Client):
120
112
121
113
def authorization_url (self , ** kwargs ):
122
114
raise NotImplemented (
123
- "You should have obtained resource owner's password, somehow. " )
115
+ "You should have already obtained resource owner's password" )
124
116
125
- def get_token (self , username , password , ** kwargs ):
117
+ def get_token (self , username , password , scope = None , ** kwargs ):
126
118
return super (ResourceOwnerPasswordCredentialsGrant , self ).get_token (
127
- "password" , username = username , password = password , ** kwargs )
119
+ "password" , username = username , password = password , scope = scope ,
120
+ ** kwargs )
128
121
129
122
130
123
class ClientCredentialGrant (Client ):
131
124
def authorization_url (self , ** kwargs ):
132
- raise NotImplemented (
133
- # Since the client authentication is used as the authorization grant
134
- "No additional authorization request is needed" )
125
+ # Since the client authentication is used as the authorization grant
126
+ raise NotImplemented ("No additional authorization request is needed" )
135
127
136
- def get_token (self , ** kwargs ):
128
+ def get_token (self , scope = None , ** kwargs ):
137
129
return super (ClientCredentialGrant , self ).get_token (
138
- "client_credentials" , ** kwargs )
130
+ "client_credentials" , scope = scope , ** kwargs )
139
131
0 commit comments