@@ -70,7 +70,7 @@ def authorize_redirect(self, uri):
7070 return redirect (urllib .parse .quote (redirect_url ))
7171
7272
73- class MockOAuth :
73+ class MockAuthlibFlaskClient :
7474 def __init__ (self ):
7575 pass
7676
@@ -83,7 +83,9 @@ def register(self, name, **kwargs):
8383def test_github (app , sqlalchemy_datastore , get_message ):
8484 CSRFProtect (app )
8585 init_app_with_options (
86- app , sqlalchemy_datastore , ** {"security_args" : {"oauth" : MockOAuth ()}}
86+ app ,
87+ sqlalchemy_datastore ,
88+ ** {"security_args" : {"oauth" : MockAuthlibFlaskClient ()}},
8789 )
8890 client = app .test_client ()
8991 response = client .get ("/login" )
@@ -115,7 +117,9 @@ def test_github_nocsrf(app, sqlalchemy_datastore, get_message):
115117 # Test if ignore_unauth_endpoints is true - doesn't require CSRF
116118 CSRFProtect (app )
117119 init_app_with_options (
118- app , sqlalchemy_datastore , ** {"security_args" : {"oauth" : MockOAuth ()}}
120+ app ,
121+ sqlalchemy_datastore ,
122+ ** {"security_args" : {"oauth" : MockAuthlibFlaskClient ()}},
119123 )
120124 client = app .test_client ()
121125 response = client .get ("/login" )
@@ -131,7 +135,7 @@ def myoauth_fetch_identity(oauth, token):
131135 profile = resp .json ()
132136 return "email" , profile ["email" ]
133137
134- authlib_oauth = MockOAuth ()
138+ authlib_oauth = MockAuthlibFlaskClient ()
135139 authlib_oauth .register ("myoauth" )
136140 init_app_with_options (
137141 app , sqlalchemy_datastore , ** {"security_args" : {"oauth" : authlib_oauth }}
@@ -157,7 +161,9 @@ def myoauth_fetch_identity(oauth, token):
157161@pytest .mark .settings (oauth_enable = True )
158162def test_bad_api (app , sqlalchemy_datastore , get_message ):
159163 init_app_with_options (
160- app , sqlalchemy_datastore , ** {"security_args" : {"oauth" : MockOAuth ()}}
164+ app ,
165+ sqlalchemy_datastore ,
166+ ** {"security_args" : {"oauth" : MockAuthlibFlaskClient ()}},
161167 )
162168 client = app .test_client ()
163169
@@ -186,7 +192,9 @@ def test_bad_api(app, sqlalchemy_datastore, get_message):
186192@pytest .mark .settings (oauth_enable = True )
187193def test_unknown_user (app , sqlalchemy_datastore , get_message ):
188194 init_app_with_options (
189- app , sqlalchemy_datastore , ** {"security_args" : {"oauth" : MockOAuth ()}}
195+ app ,
196+ sqlalchemy_datastore ,
197+ ** {"security_args" : {"oauth" : MockAuthlibFlaskClient ()}},
190198 )
191199 client = app .test_client ()
192200 oauth_app = app .security .oauthglue .oauth_app
@@ -198,7 +206,9 @@ def test_unknown_user(app, sqlalchemy_datastore, get_message):
198206@pytest .mark .settings (oauth_enable = True )
199207def test_disabled_user (app , sqlalchemy_datastore , get_message ):
200208 init_app_with_options (
201- app , sqlalchemy_datastore , ** {"security_args" : {"oauth" : MockOAuth ()}}
209+ app ,
210+ sqlalchemy_datastore ,
211+ ** {"security_args" : {"oauth" : MockAuthlibFlaskClient ()}},
202212 )
203213 client = app .test_client ()
204214 oauth_app = app .security .oauthglue .oauth_app
@@ -208,6 +218,44 @@ def test_disabled_user(app, sqlalchemy_datastore, get_message):
208218 assert get_message ("DISABLED_ACCOUNT" ) in response .data # should be in flash
209219
210220
221+ @pytest .mark .settings (oauth_enable = True )
222+ def test_unicode_email (app , sqlalchemy_datastore , get_message ):
223+ # Test that we normalize email response from oauth provider
224+ init_app_with_options (
225+ app ,
226+ sqlalchemy_datastore ,
227+ ** {"security_args" : {"oauth" : MockAuthlibFlaskClient ()}},
228+ )
229+ client = app .test_client ()
230+ oauth_app = app .security .oauthglue .oauth_app
231+ oauth_app .github .set_identity (
232+ "matt@\N{FULLWIDTH LATIN SMALL LETTER L} \N{FULLWIDTH LATIN SMALL LETTER P} .com"
233+ )
234+ client .get ("/login/oauthresponse/github" , follow_redirects = True )
235+ assert is_authenticated (client , get_message )
236+
237+
238+ @pytest .mark .settings (
239+ oauth_enable = True ,
240+ USER_IDENTITY_ATTRIBUTES = [
241+ {"username" : {"mapper" : lambda x : x }},
242+ ],
243+ )
244+ def test_email_not_identity (app , sqlalchemy_datastore , get_message ):
245+ # Test that if email isn't an identity attribute - can't use oauth to log in
246+ init_app_with_options (
247+ app ,
248+ sqlalchemy_datastore ,
249+ ** {"security_args" : {"oauth" : MockAuthlibFlaskClient ()}},
250+ )
251+ client = app .test_client ()
252+ response = client .get ("/login/oauthresponse/github" , follow_redirects = True )
253+ assert (
254+ b": (Config error - Invalid identity attribute: email). Please try again."
255+ in response .data
256+ )
257+
258+
211259def _locked (self , form_error ):
212260 if self .email == "gal@lp.com" :
213261 form_error .append ("You are not allowed to do that" )
@@ -219,7 +267,9 @@ def _locked(self, form_error):
219267@pytest .mark .settings (oauth_enable = True )
220268def test_override_user_locked (app , sqlalchemy_datastore , get_message ):
221269 init_app_with_options (
222- app , sqlalchemy_datastore , ** {"security_args" : {"oauth" : MockOAuth ()}}
270+ app ,
271+ sqlalchemy_datastore ,
272+ ** {"security_args" : {"oauth" : MockAuthlibFlaskClient ()}},
223273 )
224274 client = app .test_client ()
225275 oauth_app = app .security .oauthglue .oauth_app
@@ -233,7 +283,9 @@ def test_override_user_locked(app, sqlalchemy_datastore, get_message):
233283@pytest .mark .settings (oauth_enable = True )
234284def test_tf (app , sqlalchemy_datastore , get_message ):
235285 init_app_with_options (
236- app , sqlalchemy_datastore , ** {"security_args" : {"oauth" : MockOAuth ()}}
286+ app ,
287+ sqlalchemy_datastore ,
288+ ** {"security_args" : {"oauth" : MockAuthlibFlaskClient ()}},
237289 )
238290 client = app .test_client ()
239291 authenticate (client )
@@ -273,7 +325,9 @@ def test_spa(app, sqlalchemy_datastore, get_message):
273325 headers = {"Accept" : "application/json" , "Content-Type" : "application/json" }
274326
275327 init_app_with_options (
276- app , sqlalchemy_datastore , ** {"security_args" : {"oauth" : MockOAuth ()}}
328+ app ,
329+ sqlalchemy_datastore ,
330+ ** {"security_args" : {"oauth" : MockAuthlibFlaskClient ()}},
277331 )
278332 client = app .test_client ()
279333 csrf_token = get_csrf_token (client )
@@ -325,7 +379,9 @@ def test_spa(app, sqlalchemy_datastore, get_message):
325379def test_already_auth (app , sqlalchemy_datastore , get_message ):
326380 headers = {"Accept" : "application/json" , "Content-Type" : "application/json" }
327381 init_app_with_options (
328- app , sqlalchemy_datastore , ** {"security_args" : {"oauth" : MockOAuth ()}}
382+ app ,
383+ sqlalchemy_datastore ,
384+ ** {"security_args" : {"oauth" : MockAuthlibFlaskClient ()}},
329385 )
330386 client = app .test_client ()
331387 authenticate (client )
@@ -346,7 +402,9 @@ def test_simple_next(app, sqlalchemy_datastore, get_message):
346402 # For oauth we stash 'next' in the session since we can't really
347403 # send it all around the oauth providers.
348404 init_app_with_options (
349- app , sqlalchemy_datastore , ** {"security_args" : {"oauth" : MockOAuth ()}}
405+ app ,
406+ sqlalchemy_datastore ,
407+ ** {"security_args" : {"oauth" : MockAuthlibFlaskClient ()}},
350408 )
351409 client = app .test_client ()
352410 response = client .get ("/profile" , follow_redirects = True )
@@ -379,7 +437,9 @@ def oauth_response_failure(self, error_view, e):
379437 return redirect ("/uh-oh" )
380438
381439 init_app_with_options (
382- app , sqlalchemy_datastore , ** {"security_args" : {"oauth" : MockOAuth ()}}
440+ app ,
441+ sqlalchemy_datastore ,
442+ ** {"security_args" : {"oauth" : MockAuthlibFlaskClient ()}},
383443 )
384444 # Have to register with Oauthglue.
385445 app .security .oauthglue .register_provider_ext (MyOauthProvider ("myoauth" ))
@@ -410,7 +470,9 @@ def oauth_response_failure(self, error_view, e):
410470@pytest .mark .settings (oauth_enable = True , post_verify_view = "/post_verify" )
411471def test_verify (app , sqlalchemy_datastore , get_message ):
412472 init_app_with_options (
413- app , sqlalchemy_datastore , ** {"security_args" : {"oauth" : MockOAuth ()}}
473+ app ,
474+ sqlalchemy_datastore ,
475+ ** {"security_args" : {"oauth" : MockAuthlibFlaskClient ()}},
414476 )
415477 client = app .test_client ()
416478
@@ -444,7 +506,9 @@ def test_verify(app, sqlalchemy_datastore, get_message):
444506@pytest .mark .settings (oauth_enable = True )
445507def test_verify_unknown_user (app , sqlalchemy_datastore , get_message ):
446508 init_app_with_options (
447- app , sqlalchemy_datastore , ** {"security_args" : {"oauth" : MockOAuth ()}}
509+ app ,
510+ sqlalchemy_datastore ,
511+ ** {"security_args" : {"oauth" : MockAuthlibFlaskClient ()}},
448512 )
449513 client = app .test_client ()
450514 authenticate (client )
@@ -469,6 +533,38 @@ def test_verify_unknown_user(app, sqlalchemy_datastore, get_message):
469533 assert "fs_oauth_next" not in session
470534
471535
536+ @pytest .mark .settings (oauth_enable = True )
537+ def test_verify_evil_user (app , sqlalchemy_datastore , get_message ):
538+ # Ensure a valid (evil) user can't use their oauth credentials to 'verify'
539+ # another user.
540+ init_app_with_options (
541+ app ,
542+ sqlalchemy_datastore ,
543+ ** {"security_args" : {"oauth" : MockAuthlibFlaskClient ()}},
544+ )
545+ client = app .test_client ()
546+ authenticate (client ) # authenticate as matt@lp.com
547+ oauth_app = app .security .oauthglue .oauth_app
548+ oauth_app .github .set_identity ("joe@lp.com" ) # a valid user
549+
550+ reset_fresh (client , app .config ["SECURITY_FRESHNESS" ])
551+ response = client .get ("/fresh" , follow_redirects = True )
552+ github_url = get_form_action (response , 1 )
553+ response = client .post (github_url , follow_redirects = False )
554+
555+ with capture_flashes () as flashes :
556+ redirect_url = urllib .parse .urlsplit (urllib .parse .unquote (response .location ))
557+ local_redirect = urllib .parse .parse_qs (redirect_url .query )["redirect_uri" ][0 ]
558+ response = client .get (local_redirect , follow_redirects = True )
559+ assert flashes [0 ]["category" ] == "error"
560+ assert flashes [0 ]["message" ].encode ("utf-8" ) == get_message (
561+ "IDENTITY_NOT_REGISTERED" , id = "joe@lp.com"
562+ )
563+ assert b"Reauthenticate" in response .data
564+ session = get_session (response )
565+ assert "fs_oauth_next" not in session
566+
567+
472568@pytest .mark .settings (
473569 oauth_enable = True ,
474570 oauth_builtin_providers = ["github" ],
@@ -479,7 +575,9 @@ def test_verify_unknown_user(app, sqlalchemy_datastore, get_message):
479575def test_verify_spa (app , sqlalchemy_datastore , get_message ):
480576 headers = {"Accept" : "application/json" , "Content-Type" : "application/json" }
481577 init_app_with_options (
482- app , sqlalchemy_datastore , ** {"security_args" : {"oauth" : MockOAuth ()}}
578+ app ,
579+ sqlalchemy_datastore ,
580+ ** {"security_args" : {"oauth" : MockAuthlibFlaskClient ()}},
483581 )
484582 client = app .test_client ()
485583 json_authenticate (client )
@@ -506,7 +604,9 @@ def test_verify_spa(app, sqlalchemy_datastore, get_message):
506604)
507605def test_verify_unknown_user_spa (app , sqlalchemy_datastore , get_message ):
508606 init_app_with_options (
509- app , sqlalchemy_datastore , ** {"security_args" : {"oauth" : MockOAuth ()}}
607+ app ,
608+ sqlalchemy_datastore ,
609+ ** {"security_args" : {"oauth" : MockAuthlibFlaskClient ()}},
510610 )
511611 client = app .test_client ()
512612 json_authenticate (client )
@@ -526,7 +626,9 @@ def test_verify_unknown_user_spa(app, sqlalchemy_datastore, get_message):
526626)
527627def test_verify_spa_exc (app , sqlalchemy_datastore , get_message ):
528628 init_app_with_options (
529- app , sqlalchemy_datastore , ** {"security_args" : {"oauth" : MockOAuth ()}}
629+ app ,
630+ sqlalchemy_datastore ,
631+ ** {"security_args" : {"oauth" : MockAuthlibFlaskClient ()}},
530632 )
531633 client = app .test_client ()
532634 json_authenticate (client )
0 commit comments