test_authorize.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749
  1. import base64
  2. import json
  3. from urllib.parse import urlparse, parse_qs
  4. from flask import url_for
  5. from app.db import Session
  6. from app.jose_utils import verify_id_token, decode_id_token
  7. from app.models import Client, User, ClientUser, RedirectUri
  8. from app.oauth.views.authorize import (
  9. get_host_name_and_scheme,
  10. generate_access_token,
  11. construct_url,
  12. )
  13. from tests.utils import login, random_domain, random_string, random_email
  14. def generate_random_uri() -> str:
  15. return f"https://{random_domain()}/callback"
  16. def test_get_host_name_and_scheme():
  17. assert get_host_name_and_scheme("http://localhost:8000?a=b") == (
  18. "localhost",
  19. "http",
  20. )
  21. assert get_host_name_and_scheme(
  22. "https://www.bubblecode.net/en/2016/01/22/understanding-oauth2/#Implicit_Grant"
  23. ) == ("www.bubblecode.net", "https")
  24. def test_generate_access_token(flask_client):
  25. access_token = generate_access_token()
  26. assert len(access_token) == 40
  27. def test_construct_url():
  28. url = construct_url("http://ab.cd", {"x": "1 2"})
  29. assert url == "http://ab.cd?x=1%202"
  30. def test_authorize_page_non_login_user(flask_client):
  31. """make sure to display login page for non-authenticated user"""
  32. user = User.create(random_email(), random_string())
  33. Session.commit()
  34. client = Client.create_new(random_string(), user.id)
  35. Session.commit()
  36. uri = generate_random_uri()
  37. RedirectUri.create(
  38. client_id=client.id,
  39. uri=uri,
  40. commit=True,
  41. )
  42. r = flask_client.get(
  43. url_for(
  44. "oauth.authorize",
  45. client_id=client.oauth_client_id,
  46. state="teststate",
  47. redirect_uri=uri,
  48. response_type="code",
  49. )
  50. )
  51. html = r.get_data(as_text=True)
  52. assert r.status_code == 200
  53. assert "Sign in to accept sharing data with" in html
  54. def test_authorize_page_login_user_non_supported_flow(flask_client):
  55. """return 400 if the flow is not supported"""
  56. user = login(flask_client)
  57. client = Client.create_new("test client", user.id)
  58. Session.commit()
  59. # Not provide any flow
  60. r = flask_client.get(
  61. url_for(
  62. "oauth.authorize",
  63. client_id=client.oauth_client_id,
  64. state="teststate",
  65. redirect_uri="http://localhost",
  66. # not provide response_type param here
  67. )
  68. )
  69. # Provide a not supported flow
  70. html = r.get_data(as_text=True)
  71. assert r.status_code == 400
  72. assert "SimpleLogin only support the following OIDC flows" in html
  73. r = flask_client.get(
  74. url_for(
  75. "oauth.authorize",
  76. client_id=client.oauth_client_id,
  77. state="teststate",
  78. redirect_uri="http://localhost",
  79. # SL does not support this flow combination
  80. response_type="code token id_token",
  81. )
  82. )
  83. html = r.get_data(as_text=True)
  84. assert r.status_code == 400
  85. assert "SimpleLogin only support the following OIDC flows" in html
  86. def test_authorize_page_login_user(flask_client):
  87. """make sure to display authorization page for authenticated user"""
  88. user = login(flask_client)
  89. client = Client.create_new("test client", user.id)
  90. Session.commit()
  91. uri = generate_random_uri()
  92. RedirectUri.create(
  93. client_id=client.id,
  94. uri=uri,
  95. commit=True,
  96. )
  97. r = flask_client.get(
  98. url_for(
  99. "oauth.authorize",
  100. client_id=client.oauth_client_id,
  101. state="teststate",
  102. redirect_uri=uri,
  103. response_type="code",
  104. )
  105. )
  106. html = r.get_data(as_text=True)
  107. assert r.status_code == 200
  108. assert f"{user.email} (Personal Email)" in html
  109. def test_authorize_code_flow_no_openid_scope(flask_client):
  110. """make sure the authorize redirects user to correct page for the *Code Flow*
  111. and when the *openid* scope is not present
  112. , ie when response_type=code, openid not in scope
  113. """
  114. user = login(flask_client)
  115. client = Client.create_new("test client", user.id)
  116. Session.commit()
  117. domain = random_domain()
  118. uri = f"https://{domain}/callback"
  119. RedirectUri.create(
  120. client_id=client.id,
  121. uri=uri,
  122. commit=True,
  123. )
  124. # user allows client on the authorization page
  125. r = flask_client.post(
  126. url_for(
  127. "oauth.authorize",
  128. client_id=client.oauth_client_id,
  129. state="teststate",
  130. redirect_uri=uri,
  131. response_type="code",
  132. ),
  133. data={"button": "allow", "suggested-email": "x@y.z", "suggested-name": "AB CD"},
  134. # user will be redirected to client page, do not allow redirection here
  135. # to assert the redirect url
  136. # follow_redirects=True,
  137. )
  138. assert r.status_code == 302 # user gets redirected back to client page
  139. # r.location will have this form http://localhost?state=teststate&code=knuyjepwvg
  140. o = urlparse(r.location)
  141. assert o.netloc == domain
  142. assert not o.fragment
  143. # parse the query, should return something like
  144. # {'state': ['teststate'], 'code': ['knuyjepwvg']}
  145. queries = parse_qs(o.query)
  146. assert len(queries) == 2
  147. assert queries["state"] == ["teststate"]
  148. assert len(queries["code"]) == 1
  149. # Exchange the code to get access_token
  150. basic_auth_headers = base64.b64encode(
  151. f"{client.oauth_client_id}:{client.oauth_client_secret}".encode()
  152. ).decode("utf-8")
  153. r = flask_client.post(
  154. url_for("oauth.token"),
  155. headers={"Authorization": "Basic " + basic_auth_headers},
  156. data={"grant_type": "authorization_code", "code": queries["code"][0]},
  157. )
  158. # r.json should have this format
  159. # {
  160. # 'access_token': 'avmhluhonsouhcwwailydwvhankspptgidoggcbu',
  161. # 'expires_in': 3600,
  162. # 'scope': '',
  163. # 'token_type': 'bearer',
  164. # 'user': {
  165. # 'avatar_url': None,
  166. # 'client': 'test client',
  167. # 'email': 'x@y.z',
  168. # 'email_verified': True,
  169. # 'id': 1,
  170. # 'name': 'AB CD'
  171. # }
  172. # }
  173. assert r.status_code == 200
  174. assert r.json["access_token"]
  175. assert r.json["expires_in"] == 3600
  176. assert not r.json["scope"]
  177. assert r.json["token_type"] == "Bearer"
  178. client_user = ClientUser.get_by(client_id=client.id)
  179. assert r.json["user"] == {
  180. "avatar_url": None,
  181. "client": "test client",
  182. "email": "x@y.z",
  183. "email_verified": True,
  184. "id": client_user.id,
  185. "name": "AB CD",
  186. "sub": str(client_user.id),
  187. }
  188. def test_authorize_code_flow_with_openid_scope(flask_client):
  189. """make sure the authorize redirects user to correct page for the *Code Flow*
  190. and when the *openid* scope is present
  191. , ie when response_type=code, openid in scope
  192. The authorize endpoint should stay the same: return the *code*.
  193. The token endpoint however should now return id_token in addition to the access_token
  194. """
  195. user = login(flask_client)
  196. client = Client.create_new("test client", user.id)
  197. Session.commit()
  198. domain = random_domain()
  199. uri = f"https://{domain}/callback"
  200. RedirectUri.create(
  201. client_id=client.id,
  202. uri=uri,
  203. commit=True,
  204. )
  205. # user allows client on the authorization page
  206. r = flask_client.post(
  207. url_for(
  208. "oauth.authorize",
  209. client_id=client.oauth_client_id,
  210. state="teststate",
  211. redirect_uri=uri,
  212. response_type="code",
  213. scope="openid", # openid is in scope
  214. ),
  215. data={"button": "allow", "suggested-email": "x@y.z", "suggested-name": "AB CD"},
  216. # user will be redirected to client page, do not allow redirection here
  217. # to assert the redirect url
  218. # follow_redirects=True,
  219. )
  220. assert r.status_code == 302 # user gets redirected back to client page
  221. # r.location will have this form http://localhost?state=teststate&code=knuyjepwvg
  222. o = urlparse(r.location)
  223. assert o.netloc == domain
  224. assert not o.fragment
  225. # parse the query, should return something like
  226. # {'state': ['teststate'], 'code': ['knuyjepwvg'], 'scope': ["openid"]}
  227. queries = parse_qs(o.query)
  228. assert len(queries) == 3
  229. assert queries["state"] == ["teststate"]
  230. assert len(queries["code"]) == 1
  231. # Exchange the code to get access_token
  232. basic_auth_headers = base64.b64encode(
  233. f"{client.oauth_client_id}:{client.oauth_client_secret}".encode()
  234. ).decode("utf-8")
  235. r = flask_client.post(
  236. url_for("oauth.token"),
  237. headers={"Authorization": "Basic " + basic_auth_headers},
  238. data={"grant_type": "authorization_code", "code": queries["code"][0]},
  239. )
  240. # r.json should have this format
  241. # {
  242. # 'access_token': 'avmhluhonsouhcwwailydwvhankspptgidoggcbu',
  243. # 'expires_in': 3600,
  244. # 'scope': '',
  245. # 'token_type': 'bearer',
  246. # 'user': {
  247. # 'avatar_url': None,
  248. # 'client': 'test client',
  249. # 'email': 'x@y.z',
  250. # 'email_verified': True,
  251. # 'id': 1,
  252. # 'name': 'AB CD'
  253. # }
  254. # }
  255. assert r.status_code == 200
  256. assert r.json["access_token"]
  257. assert r.json["expires_in"] == 3600
  258. assert r.json["scope"] == "openid"
  259. assert r.json["token_type"] == "Bearer"
  260. client_user = ClientUser.get_by(client_id=client.id)
  261. assert r.json["user"] == {
  262. "avatar_url": None,
  263. "client": "test client",
  264. "email": "x@y.z",
  265. "email_verified": True,
  266. "id": client_user.id,
  267. "name": "AB CD",
  268. "sub": str(client_user.id),
  269. }
  270. # id_token must be returned
  271. assert r.json["id_token"]
  272. # id_token must be a valid, correctly signed JWT
  273. assert verify_id_token(r.json["id_token"])
  274. def test_authorize_token_flow(flask_client):
  275. """make sure the authorize redirects user to correct page for the *Token Flow*
  276. , ie when response_type=token
  277. The /authorize endpoint should return an access_token
  278. """
  279. user = login(flask_client)
  280. client = Client.create_new("test client", user.id)
  281. Session.commit()
  282. domain = random_domain()
  283. uri = f"https://{domain}/callback"
  284. RedirectUri.create(
  285. client_id=client.id,
  286. uri=uri,
  287. commit=True,
  288. )
  289. # user allows client on the authorization page
  290. r = flask_client.post(
  291. url_for(
  292. "oauth.authorize",
  293. client_id=client.oauth_client_id,
  294. state="teststate",
  295. redirect_uri=uri,
  296. response_type="token", # token flow
  297. ),
  298. data={"button": "allow", "suggested-email": "x@y.z", "suggested-name": "AB CD"},
  299. # user will be redirected to client page, do not allow redirection here
  300. # to assert the redirect url
  301. # follow_redirects=True,
  302. )
  303. assert r.status_code == 302 # user gets redirected back to client page
  304. # r.location will have this form http://localhost?state=teststate&code=knuyjepwvg
  305. o = urlparse(r.location)
  306. assert o.netloc == domain
  307. # in token flow, access_token is in fragment and not query
  308. assert o.fragment
  309. assert not o.query
  310. # parse the fragment, should return something like
  311. # {'state': ['teststate'], 'access_token': ['knuyjepwvg']}
  312. queries = parse_qs(o.fragment)
  313. assert len(queries) == 2
  314. assert queries["state"] == ["teststate"]
  315. # access_token must be returned
  316. assert len(queries["access_token"]) == 1
  317. def test_authorize_id_token_flow(flask_client):
  318. """make sure the authorize redirects user to correct page for the *ID-Token Flow*
  319. , ie when response_type=id_token
  320. The /authorize endpoint should return an id_token
  321. """
  322. user = login(flask_client)
  323. client = Client.create_new("test client", user.id)
  324. Session.commit()
  325. domain = random_domain()
  326. uri = f"https://{domain}/callback"
  327. RedirectUri.create(
  328. client_id=client.id,
  329. uri=uri,
  330. commit=True,
  331. )
  332. # user allows client on the authorization page
  333. r = flask_client.post(
  334. url_for(
  335. "oauth.authorize",
  336. client_id=client.oauth_client_id,
  337. state="teststate",
  338. redirect_uri=uri,
  339. response_type="id_token", # id_token flow
  340. ),
  341. data={"button": "allow", "suggested-email": "x@y.z", "suggested-name": "AB CD"},
  342. # user will be redirected to client page, do not allow redirection here
  343. # to assert the redirect url
  344. # follow_redirects=True,
  345. )
  346. assert r.status_code == 302 # user gets redirected back to client page
  347. # r.location will have this form http://localhost?state=teststate&code=knuyjepwvg
  348. o = urlparse(r.location)
  349. assert o.netloc == domain
  350. assert not o.fragment
  351. assert o.query
  352. # parse the fragment, should return something like
  353. # {'state': ['teststate'], 'id_token': ['knuyjepwvg']}
  354. queries = parse_qs(o.query)
  355. assert len(queries) == 2
  356. assert queries["state"] == ["teststate"]
  357. # access_token must be returned
  358. assert len(queries["id_token"]) == 1
  359. # id_token must be a valid, correctly signed JWT
  360. assert verify_id_token(queries["id_token"][0])
  361. def test_authorize_token_id_token_flow(flask_client):
  362. """make sure the authorize redirects user to correct page for the *ID-Token Token Flow*
  363. , ie when response_type=id_token,token
  364. The /authorize endpoint should return an id_token and access_token
  365. id_token, once decoded, should contain *at_hash* in payload
  366. """
  367. user = login(flask_client)
  368. client = Client.create_new("test client", user.id)
  369. Session.commit()
  370. domain = random_domain()
  371. uri = f"https://{domain}/callback"
  372. RedirectUri.create(
  373. client_id=client.id,
  374. uri=uri,
  375. commit=True,
  376. )
  377. # user allows client on the authorization page
  378. r = flask_client.post(
  379. url_for(
  380. "oauth.authorize",
  381. client_id=client.oauth_client_id,
  382. state="teststate",
  383. redirect_uri=uri,
  384. response_type="id_token token", # id_token,token flow
  385. ),
  386. data={"button": "allow", "suggested-email": "x@y.z", "suggested-name": "AB CD"},
  387. # user will be redirected to client page, do not allow redirection here
  388. # to assert the redirect url
  389. # follow_redirects=True,
  390. )
  391. assert r.status_code == 302 # user gets redirected back to client page
  392. # r.location will have this form http://localhost?state=teststate&code=knuyjepwvg
  393. o = urlparse(r.location)
  394. assert o.netloc == domain
  395. assert o.fragment
  396. assert not o.query
  397. # parse the fragment, should return something like
  398. # {'state': ['teststate'], 'id_token': ['knuyjepwvg']}
  399. queries = parse_qs(o.fragment)
  400. assert len(queries) == 3
  401. assert queries["state"] == ["teststate"]
  402. # access_token must be returned
  403. assert len(queries["id_token"]) == 1
  404. assert len(queries["access_token"]) == 1
  405. # id_token must be a valid, correctly signed JWT
  406. id_token = queries["id_token"][0]
  407. assert verify_id_token(id_token)
  408. # make sure jwt has all the necessary fields
  409. jwt = decode_id_token(id_token)
  410. # payload should have this format
  411. # {
  412. # 'at_hash': 'jLDmoGpuOIHwxeyFEe9SKw',
  413. # 'aud': 'testclient-sywcpwsyua',
  414. # 'auth_time': 1565450736,
  415. # 'avatar_url': None,
  416. # 'client': 'test client',
  417. # 'email': 'x@y.z',
  418. # 'email_verified': True,
  419. # 'exp': 1565454336,
  420. # 'iat': 1565450736,
  421. # 'id': 1,
  422. # 'iss': 'http://localhost',
  423. # 'name': 'AB CD',
  424. # 'sub': '1'
  425. # }
  426. payload = json.loads(jwt.claims)
  427. # at_hash MUST be present when the flow is id_token,token
  428. assert "at_hash" in payload
  429. assert "aud" in payload
  430. assert "auth_time" in payload
  431. assert "avatar_url" in payload
  432. assert "client" in payload
  433. assert "email" in payload
  434. assert "email_verified" in payload
  435. assert "exp" in payload
  436. assert "iat" in payload
  437. assert "id" in payload
  438. assert "iss" in payload
  439. assert "name" in payload
  440. assert "sub" in payload
  441. def test_authorize_code_id_token_flow(flask_client):
  442. """make sure the authorize redirects user to correct page for the *ID-Token Code Flow*
  443. , ie when response_type=id_token,code
  444. The /authorize endpoint should return an id_token, code and id_token must contain *c_hash*
  445. The /token endpoint must return a access_token and an id_token
  446. """
  447. user = login(flask_client)
  448. client = Client.create_new("test client", user.id)
  449. Session.commit()
  450. domain = random_domain()
  451. uri = f"https://{domain}/callback"
  452. RedirectUri.create(
  453. client_id=client.id,
  454. uri=uri,
  455. commit=True,
  456. )
  457. # user allows client on the authorization page
  458. r = flask_client.post(
  459. url_for(
  460. "oauth.authorize",
  461. client_id=client.oauth_client_id,
  462. state="teststate",
  463. redirect_uri=uri,
  464. response_type="id_token code", # id_token,code flow
  465. ),
  466. data={"button": "allow", "suggested-email": "x@y.z", "suggested-name": "AB CD"},
  467. # user will be redirected to client page, do not allow redirection here
  468. # to assert the redirect url
  469. # follow_redirects=True,
  470. )
  471. assert r.status_code == 302 # user gets redirected back to client page
  472. # r.location will have this form http://localhost?state=teststate&code=knuyjepwvg
  473. o = urlparse(r.location)
  474. assert o.netloc == domain
  475. assert not o.fragment
  476. assert o.query
  477. # parse the query, should return something like
  478. # {'state': ['teststate'], 'id_token': ['knuyjepwvg'], 'code': ['longstring']}
  479. queries = parse_qs(o.query)
  480. assert len(queries) == 3
  481. assert queries["state"] == ["teststate"]
  482. assert len(queries["id_token"]) == 1
  483. assert len(queries["code"]) == 1
  484. # id_token must be a valid, correctly signed JWT
  485. id_token = queries["id_token"][0]
  486. assert verify_id_token(id_token)
  487. # make sure jwt has all the necessary fields
  488. jwt = decode_id_token(id_token)
  489. # payload should have this format
  490. # {
  491. # 'at_hash': 'jLDmoGpuOIHwxeyFEe9SKw',
  492. # 'aud': 'testclient-sywcpwsyua',
  493. # 'auth_time': 1565450736,
  494. # 'avatar_url': None,
  495. # 'client': 'test client',
  496. # 'email': 'x@y.z',
  497. # 'email_verified': True,
  498. # 'exp': 1565454336,
  499. # 'iat': 1565450736,
  500. # 'id': 1,
  501. # 'iss': 'http://localhost',
  502. # 'name': 'AB CD',
  503. # 'sub': '1'
  504. # }
  505. payload = json.loads(jwt.claims)
  506. # at_hash MUST be present when the flow is id_token,token
  507. assert "c_hash" in payload
  508. assert "aud" in payload
  509. assert "auth_time" in payload
  510. assert "avatar_url" in payload
  511. assert "client" in payload
  512. assert "email" in payload
  513. assert "email_verified" in payload
  514. assert "exp" in payload
  515. assert "iat" in payload
  516. assert "id" in payload
  517. assert "iss" in payload
  518. assert "name" in payload
  519. assert "sub" in payload
  520. # <<< Exchange the code to get access_token >>>
  521. basic_auth_headers = base64.b64encode(
  522. f"{client.oauth_client_id}:{client.oauth_client_secret}".encode()
  523. ).decode("utf-8")
  524. r = flask_client.post(
  525. url_for("oauth.token"),
  526. headers={"Authorization": "Basic " + basic_auth_headers},
  527. data={"grant_type": "authorization_code", "code": queries["code"][0]},
  528. )
  529. # r.json should have this format
  530. # {
  531. # 'access_token': 'avmhluhonsouhcwwailydwvhankspptgidoggcbu',
  532. # 'id_token': 'ab.cd.xy',
  533. # 'expires_in': 3600,
  534. # 'scope': '',
  535. # 'token_type': 'bearer',
  536. # 'user': {
  537. # 'avatar_url': None,
  538. # 'client': 'test client',
  539. # 'email': 'x@y.z',
  540. # 'email_verified': True,
  541. # 'id': 1,
  542. # 'name': 'AB CD'
  543. # }
  544. # }
  545. assert r.status_code == 200
  546. assert r.json["access_token"]
  547. assert r.json["expires_in"] == 3600
  548. assert not r.json["scope"]
  549. assert r.json["token_type"] == "Bearer"
  550. client_user = ClientUser.get_by(client_id=client.id)
  551. assert r.json["user"] == {
  552. "avatar_url": None,
  553. "client": "test client",
  554. "email": "x@y.z",
  555. "email_verified": True,
  556. "id": client_user.id,
  557. "name": "AB CD",
  558. "sub": str(client_user.id),
  559. }
  560. # id_token must be returned
  561. assert r.json["id_token"]
  562. # id_token must be a valid, correctly signed JWT
  563. assert verify_id_token(r.json["id_token"])
  564. def test_authorize_page_invalid_client_id(flask_client):
  565. """make sure to redirect user to redirect_url?error=invalid_client_id"""
  566. user = login(flask_client)
  567. Client.create_new("test client", user.id)
  568. Session.commit()
  569. r = flask_client.get(
  570. url_for(
  571. "oauth.authorize",
  572. client_id="invalid_client_id",
  573. state="teststate",
  574. redirect_uri="http://localhost",
  575. response_type="code",
  576. )
  577. )
  578. assert r.status_code == 302
  579. assert r.location == url_for("auth.login")
  580. def test_authorize_page_http_not_allowed(flask_client):
  581. """make sure to redirect user to redirect_url?error=http_not_allowed"""
  582. user = login(flask_client)
  583. client = Client.create_new("test client", user.id)
  584. client.approved = True
  585. Session.commit()
  586. r = flask_client.get(
  587. url_for(
  588. "oauth.authorize",
  589. client_id=client.oauth_client_id,
  590. state="teststate",
  591. redirect_uri="http://mywebsite.com",
  592. response_type="code",
  593. )
  594. )
  595. assert r.status_code == 302
  596. assert r.location == "http://mywebsite.com?error=http_not_allowed"
  597. def test_authorize_page_unknown_redirect_uri(flask_client):
  598. """make sure to redirect user to redirect_url?error=unknown_redirect_uri"""
  599. user = login(flask_client)
  600. client = Client.create_new("test client", user.id)
  601. client.approved = True
  602. Session.commit()
  603. r = flask_client.get(
  604. url_for(
  605. "oauth.authorize",
  606. client_id=client.oauth_client_id,
  607. state="teststate",
  608. redirect_uri="https://unknown.com",
  609. response_type="code",
  610. )
  611. )
  612. assert r.status_code == 302
  613. assert r.location == "https://unknown.com?error=unknown_redirect_uri"