import copy import pytest import time from fxa.errors import ClientError class TestOauth: @pytest.mark.parametrize("client_id,scopes", [ # firefox ("5882386c6d801776", "profile:write https://identity.mozilla.com/apps/oldsync https://identity.mozilla.com/tokens/session"), # fenix ("a2270f727f45f648", "profile https://identity.mozilla.com/apps/oldsync https://identity.mozilla.com/tokens/session"), ]) def test_oauth_client_scopes(self, account, client_id, scopes): body = { "client_id": client_id, "ttl": 60, "grant_type": "fxa-credentials", "access_type": "online", "scope": scopes, } s = account.post_a("/oauth/token", body)['access_token'] account.post_a("/oauth/destroy", { "client_id": client_id, "token": s }) def test_oauth_authorization_noauth(self, account): body = { "client_id": "5882386c6d801776", "ttl": 60, "grant_type": "fxa-credentials", "access_type": "online", "scope": "profile", } with pytest.raises(ClientError) as e: account.post("/oauth/authorization", body) assert e.value.details == { 'code': 401, 'errno': 109, 'error': 'Unauthorized', 'message': 'invalid request signature' } @pytest.mark.parametrize("args", [ { "client_id": "5882386c6d801776", "state": "", "scope": "profile", "access_type": "invalid", "code_challenge": "", "code_challenge_method": "S256", "response_type": "code" }, { "client_id": "5882386c6d801776", "state": "", "scope": "profile", "access_type": "online", "code_challenge": "", "code_challenge_method": "invalid", "response_type": "code" }, { "client_id": "5882386c6d801776", "state": "", "scope": "profile", "access_type": "online", "code_challenge": "", "code_challenge_method": "S256", "response_type": "invalid" }, ]) def test_oauth_authorization_invalid(self, account, args): with pytest.raises(ClientError) as e: account.post_a("/oauth/authorization", args) assert e.value.details == { 'code': 400, 'errno': 107, 'error': 'Bad Request', 'message': 'invalid parameter in request body' } def test_oauth_authorization_badclientid(self, account): args = { "client_id": "invalid", "state": "", "scope": "profile", "access_type": "online", "code_challenge": "", "code_challenge_method": "S256", "response_type": "code" } with pytest.raises(ClientError) as e: account.post_a("/oauth/authorization", args) assert e.value.details == { 'code': 400, 'errno': 162, 'error': 'Bad Request', 'message': 'unknown client_id' } def test_oauth_authorization_badscope(self, account): args = { "client_id": "5882386c6d801776", "state": "", "scope": "invalid", "access_type": "online", "code_challenge": "", "code_challenge_method": "S256", "response_type": "code" } with pytest.raises(ClientError) as e: account.post_a("/oauth/authorization", args) assert e.value.details == { 'code': 400, 'errno': 169, 'error': 'Bad Request', 'message': 'requested scopes not allowed' } # see below for combined /authorization + unauthed /token def test_oauth_destroy_notoken(self, account): args = { "client_id": "5882386c6d801776", "token": "00" * 32 } with pytest.raises(ClientError) as e: account.post("/oauth/destroy", args) assert e.value.details == { 'code': 400, 'errno': 107, 'error': 'Bad Request', 'message': 'invalid parameter in request body' } @pytest.mark.xfail def test_oauth_destroy_badclient(self, account, refresh_token): args = { "client_id": "other", "token": refresh_token.bearer } with pytest.raises(ClientError) as e: account.post("/oauth/destroy", args) assert e.value.details == { 'code': 400, 'errno': 107, 'error': 'Bad Request', 'message': 'invalid parameter in request body' } def test_oauth_scoped_keys_badclient(self, account): with pytest.raises(ClientError) as e: account.post_a("/account/scoped-key-data", { "client_id": "invalid", "scope": "https://identity.mozilla.com/apps/oldsync" }) assert e.value.details == { 'code': 400, 'errno': 162, 'error': 'Bad Request', 'message': 'unknown client_id' } def test_oauth_scoped_keys_badscope(self, account): with pytest.raises(ClientError) as e: account.post_a("/account/scoped-key-data", { "client_id": "5882386c6d801776", "scope": "scope" }) assert e.value.details == { 'code': 400, 'errno': 169, 'error': 'Bad Request', 'message': 'requested scopes not allowed' } def test_oauth_scoped_keys(self, account): resp = account.post_a("/account/scoped-key-data", { "client_id": "5882386c6d801776", "scope": "https://identity.mozilla.com/apps/oldsync" }) assert resp == { "https://identity.mozilla.com/apps/oldsync": { "identifier": "https://identity.mozilla.com/apps/oldsync", "keyRotationSecret": "00" * 32, "keyRotationTimestamp": 0, }, } @pytest.mark.parametrize("access_type", ["online", "offline"]) def test_oauth_token_fxa_badclient(self, account, access_type): body = { "client_id": "invalid", "ttl": 60, "grant_type": "fxa-credentials", "access_type": access_type, "scope": "profile" } with pytest.raises(ClientError) as e: account.post_a("/oauth/token", body) assert e.value.details == { 'code': 400, 'errno': 162, 'error': 'Bad Request', 'message': 'unknown client_id' } @pytest.mark.parametrize("access_type", ["online", "offline"]) def test_oauth_token_fxa_badscope(self, account, access_type): body = { "client_id": "5882386c6d801776", "ttl": 60, "grant_type": "fxa-credentials", "access_type": access_type, "scope": "scope" } with pytest.raises(ClientError) as e: account.post_a("/oauth/token", body) assert e.value.details == { 'code': 400, 'errno': 169, 'error': 'Bad Request', 'message': 'requested scopes not allowed' } @pytest.mark.parametrize("args,code,error,errno,message", [ ({ "client_id": "5882386c6d801776", "ttl": 60, "grant_type": "authorization_code", "code": "invalid", "code_verifier": "test" }, 400, 'Bad Request', 107, 'invalid parameter in request body'), ({ "client_id": "5882386c6d801776", "ttl": 60, "grant_type": "authorization_code", "code_verifier": "test", "extra": 0 }, 400, 'Bad Request', 107, 'invalid parameter in request body'), ({ "client_id": "5882386c6d801776", "ttl": 60, "grant_type": "authorization_code", "code": "00" * 32, "code_verifier": "test" }, 401, 'Unauthorized', 110, 'invalid authentication token'), ({ "client_id": "invalid", "ttl": 60, "grant_type": "authorization_code", "code_verifier": "test" }, 400, 'Bad Request', 162, 'unknown client_id'), ({ "client_id": "5882386c6d801776", "ttl": 60, "grant_type": "authorization_code", "code_verifier": "invalid" }, 400, 'Bad Request', 107, 'invalid parameter in request body'), ]) def test_oauth_token_other_invalidcode(self, account, args, code, error, errno, message, auth_code): args = copy.deepcopy(args) if 'code' not in args: args['code'] = auth_code with pytest.raises(ClientError) as e: account.post("/oauth/token", args) assert e.value.details == { 'code': code, 'errno': errno, 'error': error, 'message': message } @pytest.mark.parametrize("args,code,error,errno,message", [ ({ "client_id": "5882386c6d801776", "ttl": 60, "grant_type": "fxa-credentials", "scope": "profile", "access_type": "online" }, 400, 'Bad Request', 107, 'invalid parameter in request body'), ({ "client_id": "5882386c6d801776", "ttl": 60, "grant_type": "refresh_token", "refresh_token": "invalid", "code_verifier": "test" }, 400, 'Bad Request', 107, 'invalid parameter in request body'), ({ "client_id": "5882386c6d801776", "ttl": 60, "grant_type": "refresh_token", "scope": "profile", "extra": 0 }, 400, 'Bad Request', 107, 'invalid parameter in request body'), ({ "client_id": "invalid", "ttl": 60, "grant_type": "refresh_token", "scope": "profile" }, 400, 'Bad Request', 162, 'unknown client_id'), ({ "client_id": "5882386c6d801776", "ttl": 60, "grant_type": "refresh_token", "scope": "foo" }, 400, 'Bad Request', 169, 'requested scopes not allowed'), ({ "client_id": "5882386c6d801776", "ttl": 60, "grant_type": "refresh_token", "scope": "profile:write" }, 400, 'Bad Request', 169, 'requested scopes not allowed'), ]) def test_oauth_token_other_invalidrefresh(self, account, args, code, error, errno, message, refresh_token): args = copy.deepcopy(args) if 'refresh_token' not in args: args['refresh_token'] = refresh_token.bearer with pytest.raises(ClientError) as e: account.post("/oauth/token", args) assert e.value.details == { 'code': code, 'errno': errno, 'error': error, 'message': message } @pytest.mark.parametrize("refresh", [False, True]) def test_oauth_fxa(self, account, refresh): body = { "client_id": "5882386c6d801776", "ttl": 60, "grant_type": "fxa-credentials", "access_type": "offline" if refresh else "online", "scope": "profile", } resp = account.post_a("/oauth/token", body) assert 'access_token' in resp assert ('refresh_token' in resp) == refresh assert 'session_token' not in resp assert resp['scope'] == 'profile' assert resp['token_type'] == 'bearer' assert resp['expires_in'] <= 60 assert (resp['auth_at'] - time.time()) < 10 assert 'keys_jwe' not in resp @pytest.mark.parametrize("keys_jwe", [None, "keyskeyskeys"]) @pytest.mark.parametrize("refresh,session", [ (False, False), (True, False), (True, True), ]) def test_oauth_auth_code(self, account, keys_jwe, refresh, session): scope = "profile" + (" https://identity.mozilla.com/tokens/session" if session else "") body = { "client_id": "5882386c6d801776", "state": "test", "keys_jwe": keys_jwe, "scope": scope, "access_type": "offline" if refresh else "online", "code_challenge": "n4bQgYhMfWWaL-qgxVrQFaO_TxsrC4Is0V1sFbDwCgg", # "test" "code_challenge_method": "S256", "response_type": "code", } resp = account.post_a("/oauth/authorization", body) assert resp['state'] == "test" body = { "client_id": "5882386c6d801776", "ttl": 60, "grant_type": "authorization_code", "code": resp['code'], "code_verifier": "test", } resp = account.post("/oauth/token", body) assert 'access_token' in resp assert ('refresh_token' in resp) == refresh assert ('session_token' in resp) == session assert resp['scope'] == 'profile' assert resp['token_type'] == 'bearer' assert resp['expires_in'] <= 60 assert (resp['auth_at'] - time.time()) < 10 assert keys_jwe is None or (resp['keys_jwe'] == keys_jwe) def test_oauth_refresh(self, account, refresh_token): body = { "client_id": "5882386c6d801776", "ttl": 60, "grant_type": "refresh_token", "refresh_token": refresh_token.bearer, "scope": "profile", } resp = account.post("/oauth/token", body) assert 'access_token' in resp assert 'refresh_token' not in resp assert 'session_token' not in resp assert resp['scope'] == 'profile' assert resp['token_type'] == 'bearer' assert resp['expires_in'] <= 60 assert (resp['auth_at'] - time.time()) < 10 assert 'keys_jwe' not in resp @pytest.mark.parametrize("grant_type,access_type", [ ("authorization_code", "online"), ("refresh_token", "online"), ("fxa-credentials", "foo"), ]) def test_oauth_token_fxa_invalid(self, account, grant_type, access_type): body = { "client_id": "5882386c6d801776", "ttl": 60, "grant_type": grant_type, "access_type": access_type, "scope": "scope" } with pytest.raises(ClientError) as e: account.post_a("/oauth/token", body) assert e.value.details == { 'code': 400, 'errno': 107, 'error': 'Bad Request', 'message': 'invalid parameter in request body' } def test_oauth_authorization_unverified(unverified_account): body = { "client_id": "5882386c6d801776", "ttl": 60, "grant_type": "fxa-credentials", "access_type": "online", "scope": "profile", } with pytest.raises(ClientError) as e: unverified_account.post_a("/oauth/authorization", body) assert e.value.details == { 'code': 400, 'errno': 138, 'error': 'Bad Request', 'message': 'unverified session' } def test_oauth_scoped_unverified(unverified_account): with pytest.raises(ClientError) as e: unverified_account.post_a("/account/scoped-key-data", { "client_id": "5882386c6d801776", "scope": "https://identity.mozilla.com/apps/oldsync" }) assert e.value.details == { 'code': 400, 'errno': 138, 'error': 'Bad Request', 'message': 'unverified session' } def test_oauth_token_unverified(unverified_account): body = { "client_id": "5882386c6d801776", "ttl": 60, "grant_type": "fxa-credentials", "access_type": "online", "scope": "profile" } with pytest.raises(ClientError) as e: unverified_account.post_a("/oauth/token", body) assert e.value.details == { 'code': 400, 'errno': 138, 'error': 'Bad Request', 'message': 'unverified session' } @pytest.fixture def auth_code(account): body = { "client_id": "5882386c6d801776", "state": "test", "scope": "profile", "access_type": "online", "code_challenge": "n4bQgYhMfWWaL-qgxVrQFaO_TxsrC4Is0V1sFbDwCgg", # "test" "code_challenge_method": "S256", "response_type": "code", } return account.post_a("/oauth/authorization", body)['code']