From 77e0cb5e0053787084bcfa75da96da3bedb8de5c Mon Sep 17 00:00:00 2001 From: aeneasr <3372410+aeneasr@users.noreply.github.com> Date: Fri, 31 Dec 2021 19:57:52 +0200 Subject: [PATCH] test: add tests and fixtures for client handler --- CONTRIBUTORS | 272 +++++++++++++ ...d=DELETE-with_a_different_client_auth.json | 7 + ...elfservice-method=DELETE-without_auth.json | 7 + ...-method=DELETE-without_incorrect_auth.json | 7 + ...thod=GET-with_a_different_client_auth.json | 7 + ...t=selfservice-method=GET-without_auth.json | 7 + ...ice-method=GET-without_incorrect_auth.json | 7 + ...thod=PUT-with_a_different_client_auth.json | 7 + ...t=selfservice-method=PUT-without_auth.json | 7 + ...ice-method=PUT-without_incorrect_auth.json | 7 + ...e_clients-case=0-description=standard.json | 25 ++ ...nts-case=1-description=metadata_fails.json | 4 + ...case=2-description=short_secret_fails.json | 4 + ...tching_client_existing-endpoint=admin.json | 27 ++ ...=selfservice-with_correct_client_auth.json | 26 ++ ...hich_does_not_exist-path=-clients-foo.json | 7 + ...-path=-connect-register?client_id=foo.json | 7 + client/doc.go | 24 +- client/handler.go | 178 ++++---- client/handler_test.go | 382 ++++++++++-------- driver/registry_base.go | 4 + x/authenticator.go | 3 +- 22 files changed, 753 insertions(+), 273 deletions(-) create mode 100644 CONTRIBUTORS create mode 100644 client/.snapshots/TestHandler-case=selfservice_with_incorrect_or_missing_auth-endpoint=selfservice-method=DELETE-with_a_different_client_auth.json create mode 100644 client/.snapshots/TestHandler-case=selfservice_with_incorrect_or_missing_auth-endpoint=selfservice-method=DELETE-without_auth.json create mode 100644 client/.snapshots/TestHandler-case=selfservice_with_incorrect_or_missing_auth-endpoint=selfservice-method=DELETE-without_incorrect_auth.json create mode 100644 client/.snapshots/TestHandler-case=selfservice_with_incorrect_or_missing_auth-endpoint=selfservice-method=GET-with_a_different_client_auth.json create mode 100644 client/.snapshots/TestHandler-case=selfservice_with_incorrect_or_missing_auth-endpoint=selfservice-method=GET-without_auth.json create mode 100644 client/.snapshots/TestHandler-case=selfservice_with_incorrect_or_missing_auth-endpoint=selfservice-method=GET-without_incorrect_auth.json create mode 100644 client/.snapshots/TestHandler-case=selfservice_with_incorrect_or_missing_auth-endpoint=selfservice-method=PUT-with_a_different_client_auth.json create mode 100644 client/.snapshots/TestHandler-case=selfservice_with_incorrect_or_missing_auth-endpoint=selfservice-method=PUT-without_auth.json create mode 100644 client/.snapshots/TestHandler-case=selfservice_with_incorrect_or_missing_auth-endpoint=selfservice-method=PUT-without_incorrect_auth.json create mode 100644 client/.snapshots/TestHandler-common-case=create_clients-case=0-description=standard.json create mode 100644 client/.snapshots/TestHandler-common-case=create_clients-case=1-description=metadata_fails.json create mode 100644 client/.snapshots/TestHandler-common-case=create_clients-case=2-description=short_secret_fails.json create mode 100644 client/.snapshots/TestHandler-common-case=fetching_client_existing-endpoint=admin.json create mode 100644 client/.snapshots/TestHandler-common-case=fetching_client_existing-endpoint=selfservice-with_correct_client_auth.json create mode 100644 client/.snapshots/TestHandler-common-case=fetching_client_which_does_not_exist-path=-clients-foo.json create mode 100644 client/.snapshots/TestHandler-common-case=fetching_client_which_does_not_exist-path=-connect-register?client_id=foo.json diff --git a/CONTRIBUTORS b/CONTRIBUTORS new file mode 100644 index 00000000000..a28139fb362 --- /dev/null +++ b/CONTRIBUTORS @@ -0,0 +1,272 @@ +# contributors generated by `make contributors` + +115100 <115100@users.noreply.github.com> +Ackermann Yuriy +Aeneas +Aeneas +Aeneas +Aeneas Rekkas (arekkas) +Aeneas Rekkas +Ajanthan +Akbar Uddin Kashif +Alano Terblanche +Alexander Widerberg +Alexander Widerberg +Allan Simon +Amaan Iqbal +Amir Aslaminejad +Amir Aslaminejad +Andreas Litt +André Filipe +Ankit Singh +Ante Mihalj +Anton Samoylenko +Aritz Berasarte +Arkady Bagdasarov <56913719+arkady-bagdasarov@users.noreply.github.com> +Artem Yarmoliuk +Arthur Knoepflin +Atallah khedrane +BastianHofmann +Ben Scholzen +Benjamin Tanone +Bernat Mut <1116133+monwolf@users.noreply.github.com> +Bhargav SNV <44526455+Gituser143@users.noreply.github.com> +Brandon Philips +Brian <20056195+coopbri@users.noreply.github.com> +Brian Teller +Bruno Bigras +Bruno Heridet +Chris Mack +Christian Dreier +Christian Skovholm +Corey Burmeister +Dallan Quass +Damien Bravin +Daniel Jiménez +Daniel Schellhorn +Daniel Shuy +Daniel Sutton +Dave Kushner <1289314+dkushner@users.noreply.github.com> +David +David +David Lobe +David López +David Wilkins +DennisPattmann5012 <44261569+DennisPattmann5012@users.noreply.github.com> +Dexter Chua +Dibyajyoti Behera +Dimitrij Drus +Dimitrij Drus +Divyansh Bansal +Dmitry +Dmitry Dolbik +Edward Wilde +Eric Douglas +Euan Kemp +Felix Jung +Flavio Leggio +Flori <40140792+fl0lli@users.noreply.github.com> +Frank Felhoffer +Furkan +Gajewski Dmitriy +Genchi +George Bolo +Gilbert Gilb's +Gorka Lerchundi Osa +Grant Zvolsky +Grant Zvolský +Greg Woodcock +Grigory +Hans +Harsimran Singh Maan +Helmuth Bederna <25813283+IonFoXx@users.noreply.github.com> +Hendrik Heil +Igor Zibarev +Imran Ismail +Iñigo +Iñigo +Jacek Symonowicz +Jakub Błaszczyk +Jakub Błaszczyk +James Elliott +Jamie Stackhouse +Jan +Jan Beckmann +Jay Linski +JiaLiPassion +Jimmy Stridh +Joao Carlos +Joao Carlos +Joel Pickup +John +John Wu +Josh Giles +Joshua Obasaju <41480580+obasajujoshua31@users.noreply.github.com> +Julian Tescher +Justin Clift +Kevin Minehart +Kim Neunert +Kishan B +Kostya Lepa +Kunal Parikh +L7280153 +LemurP +Lennart Rosam +Louis Laureys +Luis Pedrosa <2365589+lpedrosa@users.noreply.github.com> +Lukasz Jagiello +Luke Stoward +MOZGIII +Marco Hutzsch <39520486+marcohutzsch1234@users.noreply.github.com> +Masoud Tahmasebi +Matheus Moraes +Matt Bonnell <64976795+mbonnell-wish@users.noreply.github.com> +Matt Bonnell +Matt Drollette +Matt Vinall +Matt Vinall +Matteo Suppo +Matthew Fawcett +Maurizio +Max Köhler +Maxime Song +Mitar +Mitar +Moritz Lang +Natalia +Nathan Mills +Neeraj +Nejcraft +Nestor +Nick Otter +Nick Ufer +NickUfer +Nikita Puzankov +Nikolay Stupak +NikolaySl +ORY Continuous Integration +ORY Continuous Integration +ORY Continuous Integration +Olivier Deckers +Olivier Tremblay +Oz Haven +Patrick Barker +Patrick Tescher +Patrik +Paul Harman +Petr Jediný +Philip Nicolcev <33558528+pnicolcev-tulipretail@users.noreply.github.com> +Philip Nicolcev +Pierre-David Bélanger +Prateek Malhotra +Quentin Perez +RNBack +Ricardo Iván Vieitez Parra <3857362+corrideat@users.noreply.github.com> +Rich Wareham +Richard Zana +RikiyaFujii +Rob Smith +Roman Lytvyn +Roman Minkin +Saad Tazi +SaintMalik <37118134+saintmalik@users.noreply.github.com> +Samuele Lilli +Sawada Shota +Sawada Shota +Shadaï ALI +Shane Starcher +Shankar Dhanasekaran +Shaurya Dhadwal +Shota SAWADA +Simon Lipp +Simon-Pierre Gingras <892367+spg@users.noreply.github.com> +Smotrov Dmitriy +Stepan Rakitin +Stephan Renatus +Steve Kaliski +Sufijen Bani +Sven Neuhaus +T Venu Madhav +The Gitter Badger +Thibault Doubliez +Thomas Aidan Curran +Thomas Aidan Curran +Thomas Recloux +Thomas Stewart +Thor Marius Henrichsen +TilmanTheile <50573074+TilmanTheile@users.noreply.github.com> +Tim Sazon +Vadim +Vincent +Vinci Xu <277040271@qq.com> +Vishesh Handa +Vitaly Migunov +Vladimir Kalugin +Wei Cheng +Wojciech Kuźmiński <45849042+woojtek@users.noreply.github.com> +Wyatt Anderson +Yannick Heinrich +Yorman ����’.͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇͇Ỏ̷͖͈̞̩͎̻̫̫̜͉̠̫͕̭̭̫ ฏ๎๎๎๎๎๎๎๎๎๎๎๎๎๎๎๎๎๎๎๎๎๎๎๎๎๎๎๎๎๎๎๎๎๎๎๎๎๎๎๎๎๎๎๎๎๎๎๎๎๎๎๎๎๎๎๎๎๎๎๎๎๎๎๎๎๎๎๎๎๎๎๎๎๎๎๎๎๎๎๎๎๎๎๎๎๎๎๎๎๎๎๎๎๎๎๎๎๎ +Yuki Hirasawa <48427044+hirasawayuki@users.noreply.github.com> +Zbigniew Mandziejewicz +abusaidm +aeneasr <3372410+aeneasr@users.noreply.github.com> +aeneasr +aeneasr +aeneasr +arapaho +arekkas +arekkas +arunas-ka +aspeteRakete +bfrisbie-brex <63267486+bfrisbie-brex@users.noreply.github.com> +catper <60221155+catper@users.noreply.github.com> +cherrymu +clausdenk +darron froese +debrutal +dharmendraImprowised <72780358+DKImprowised@users.noreply.github.com> +ducksecops +emil +fazal +fazal +fjviera +fjvierap +hackerman <3372410+aeneasr@users.noreply.github.com> +hackerman +hisamura333 <0aw2794w78t0b4c@ezweb.ne.jp> +jamesnicolas +javier.viera@mindcurv.com +jayme-github +jess +jhuggett <59655877+jhuggett@users.noreply.github.com> +khevse +kobayashilin +lauri +michaelwagler +mkontani +naveenpaul1 <79908956+naveenpaul1@users.noreply.github.com> +nessita +nishanth2143 +olFi95 +phi2039 +phiremande <16595434+phiremande@users.noreply.github.com> +pike1212 +pike1212 +prateek1192 <1192prateek@gmail.com> +rickwang7712 +robhinds +sagarshah1983 +sawadashota +seremenko-wish <60801091+seremenko-wish@users.noreply.github.com> +simpleway +timothyknight +tutman96 <11356668+tutman96@users.noreply.github.com> +tyaps +vancity-amir <62674577+vancity-amir@users.noreply.github.com> +vinckr +wanderer163 <93438190+wanderer163@users.noreply.github.com> +zepatrik +zepatrik +İbrahim Esen +巢鹏 diff --git a/client/.snapshots/TestHandler-case=selfservice_with_incorrect_or_missing_auth-endpoint=selfservice-method=DELETE-with_a_different_client_auth.json b/client/.snapshots/TestHandler-case=selfservice_with_incorrect_or_missing_auth-endpoint=selfservice-method=DELETE-with_a_different_client_auth.json new file mode 100644 index 00000000000..5e96525286a --- /dev/null +++ b/client/.snapshots/TestHandler-case=selfservice_with_incorrect_or_missing_auth-endpoint=selfservice-method=DELETE-with_a_different_client_auth.json @@ -0,0 +1,7 @@ +{ + "body": { + "error": "The request could not be authorized", + "error_description": "The OAuth 2.0 Client ID from the path does not match the OAuth 2.0 Client used to authenticate the request." + }, + "status": 401 +} diff --git a/client/.snapshots/TestHandler-case=selfservice_with_incorrect_or_missing_auth-endpoint=selfservice-method=DELETE-without_auth.json b/client/.snapshots/TestHandler-case=selfservice_with_incorrect_or_missing_auth-endpoint=selfservice-method=DELETE-without_auth.json new file mode 100644 index 00000000000..73cb36f365a --- /dev/null +++ b/client/.snapshots/TestHandler-case=selfservice_with_incorrect_or_missing_auth-endpoint=selfservice-method=DELETE-without_auth.json @@ -0,0 +1,7 @@ +{ + "body": { + "error": "The request could not be authorized", + "error_description": "The requested OAuth 2.0 client does not exist or you did not provide the necessary credentials." + }, + "status": 401 +} diff --git a/client/.snapshots/TestHandler-case=selfservice_with_incorrect_or_missing_auth-endpoint=selfservice-method=DELETE-without_incorrect_auth.json b/client/.snapshots/TestHandler-case=selfservice_with_incorrect_or_missing_auth-endpoint=selfservice-method=DELETE-without_incorrect_auth.json new file mode 100644 index 00000000000..73cb36f365a --- /dev/null +++ b/client/.snapshots/TestHandler-case=selfservice_with_incorrect_or_missing_auth-endpoint=selfservice-method=DELETE-without_incorrect_auth.json @@ -0,0 +1,7 @@ +{ + "body": { + "error": "The request could not be authorized", + "error_description": "The requested OAuth 2.0 client does not exist or you did not provide the necessary credentials." + }, + "status": 401 +} diff --git a/client/.snapshots/TestHandler-case=selfservice_with_incorrect_or_missing_auth-endpoint=selfservice-method=GET-with_a_different_client_auth.json b/client/.snapshots/TestHandler-case=selfservice_with_incorrect_or_missing_auth-endpoint=selfservice-method=GET-with_a_different_client_auth.json new file mode 100644 index 00000000000..5e96525286a --- /dev/null +++ b/client/.snapshots/TestHandler-case=selfservice_with_incorrect_or_missing_auth-endpoint=selfservice-method=GET-with_a_different_client_auth.json @@ -0,0 +1,7 @@ +{ + "body": { + "error": "The request could not be authorized", + "error_description": "The OAuth 2.0 Client ID from the path does not match the OAuth 2.0 Client used to authenticate the request." + }, + "status": 401 +} diff --git a/client/.snapshots/TestHandler-case=selfservice_with_incorrect_or_missing_auth-endpoint=selfservice-method=GET-without_auth.json b/client/.snapshots/TestHandler-case=selfservice_with_incorrect_or_missing_auth-endpoint=selfservice-method=GET-without_auth.json new file mode 100644 index 00000000000..5c1758cd2c0 --- /dev/null +++ b/client/.snapshots/TestHandler-case=selfservice_with_incorrect_or_missing_auth-endpoint=selfservice-method=GET-without_auth.json @@ -0,0 +1,7 @@ +{ + "body": { + "error": "The request could not be authorized", + "error_description": "The requested OAuth 2.0 client does not exist or you did not provide the necessary credentials" + }, + "status": 401 +} diff --git a/client/.snapshots/TestHandler-case=selfservice_with_incorrect_or_missing_auth-endpoint=selfservice-method=GET-without_incorrect_auth.json b/client/.snapshots/TestHandler-case=selfservice_with_incorrect_or_missing_auth-endpoint=selfservice-method=GET-without_incorrect_auth.json new file mode 100644 index 00000000000..5c1758cd2c0 --- /dev/null +++ b/client/.snapshots/TestHandler-case=selfservice_with_incorrect_or_missing_auth-endpoint=selfservice-method=GET-without_incorrect_auth.json @@ -0,0 +1,7 @@ +{ + "body": { + "error": "The request could not be authorized", + "error_description": "The requested OAuth 2.0 client does not exist or you did not provide the necessary credentials" + }, + "status": 401 +} diff --git a/client/.snapshots/TestHandler-case=selfservice_with_incorrect_or_missing_auth-endpoint=selfservice-method=PUT-with_a_different_client_auth.json b/client/.snapshots/TestHandler-case=selfservice_with_incorrect_or_missing_auth-endpoint=selfservice-method=PUT-with_a_different_client_auth.json new file mode 100644 index 00000000000..5e96525286a --- /dev/null +++ b/client/.snapshots/TestHandler-case=selfservice_with_incorrect_or_missing_auth-endpoint=selfservice-method=PUT-with_a_different_client_auth.json @@ -0,0 +1,7 @@ +{ + "body": { + "error": "The request could not be authorized", + "error_description": "The OAuth 2.0 Client ID from the path does not match the OAuth 2.0 Client used to authenticate the request." + }, + "status": 401 +} diff --git a/client/.snapshots/TestHandler-case=selfservice_with_incorrect_or_missing_auth-endpoint=selfservice-method=PUT-without_auth.json b/client/.snapshots/TestHandler-case=selfservice_with_incorrect_or_missing_auth-endpoint=selfservice-method=PUT-without_auth.json new file mode 100644 index 00000000000..5c1758cd2c0 --- /dev/null +++ b/client/.snapshots/TestHandler-case=selfservice_with_incorrect_or_missing_auth-endpoint=selfservice-method=PUT-without_auth.json @@ -0,0 +1,7 @@ +{ + "body": { + "error": "The request could not be authorized", + "error_description": "The requested OAuth 2.0 client does not exist or you did not provide the necessary credentials" + }, + "status": 401 +} diff --git a/client/.snapshots/TestHandler-case=selfservice_with_incorrect_or_missing_auth-endpoint=selfservice-method=PUT-without_incorrect_auth.json b/client/.snapshots/TestHandler-case=selfservice_with_incorrect_or_missing_auth-endpoint=selfservice-method=PUT-without_incorrect_auth.json new file mode 100644 index 00000000000..5c1758cd2c0 --- /dev/null +++ b/client/.snapshots/TestHandler-case=selfservice_with_incorrect_or_missing_auth-endpoint=selfservice-method=PUT-without_incorrect_auth.json @@ -0,0 +1,7 @@ +{ + "body": { + "error": "The request could not be authorized", + "error_description": "The requested OAuth 2.0 client does not exist or you did not provide the necessary credentials" + }, + "status": 401 +} diff --git a/client/.snapshots/TestHandler-common-case=create_clients-case=0-description=standard.json b/client/.snapshots/TestHandler-common-case=create_clients-case=0-description=standard.json new file mode 100644 index 00000000000..eca4d4d855a --- /dev/null +++ b/client/.snapshots/TestHandler-common-case=create_clients-case=0-description=standard.json @@ -0,0 +1,25 @@ +{ + "client_id": "standard-client", + "client_name": "", + "client_secret": "averylongsecret", + "redirect_uris": [ + "http://localhost:3000/cb" + ], + "grant_types": null, + "response_types": null, + "scope": "offline_access offline openid", + "audience": [], + "owner": "", + "policy_uri": "", + "allowed_cors_origins": [], + "tos_uri": "", + "client_uri": "", + "logo_uri": "", + "contacts": null, + "client_secret_expires_at": 0, + "subject_type": "public", + "jwks": {}, + "token_endpoint_auth_method": "client_secret_basic", + "userinfo_signed_response_alg": "none", + "metadata": {} +} diff --git a/client/.snapshots/TestHandler-common-case=create_clients-case=1-description=metadata_fails.json b/client/.snapshots/TestHandler-common-case=create_clients-case=1-description=metadata_fails.json new file mode 100644 index 00000000000..378b2243d22 --- /dev/null +++ b/client/.snapshots/TestHandler-common-case=create_clients-case=1-description=metadata_fails.json @@ -0,0 +1,4 @@ +{ + "error": "invalid_client_metadata", + "error_description": "The value of one of the Client Metadata fields is invalid and the server has rejected this request. Note that an Authorization Server MAY choose to substitute a valid value for any requested parameter of a Client's Metadata. metadata cannot be set for dynamic client registration'" +} diff --git a/client/.snapshots/TestHandler-common-case=create_clients-case=2-description=short_secret_fails.json b/client/.snapshots/TestHandler-common-case=create_clients-case=2-description=short_secret_fails.json new file mode 100644 index 00000000000..b1b5ff4e6ee --- /dev/null +++ b/client/.snapshots/TestHandler-common-case=create_clients-case=2-description=short_secret_fails.json @@ -0,0 +1,4 @@ +{ + "error": "invalid_client_metadata", + "error_description": "The value of one of the Client Metadata fields is invalid and the server has rejected this request. Note that an Authorization Server MAY choose to substitute a valid value for any requested parameter of a Client's Metadata. Field client_secret must contain a secret that is at least 6 characters long." +} diff --git a/client/.snapshots/TestHandler-common-case=fetching_client_existing-endpoint=admin.json b/client/.snapshots/TestHandler-common-case=fetching_client_existing-endpoint=admin.json new file mode 100644 index 00000000000..4c93678aba6 --- /dev/null +++ b/client/.snapshots/TestHandler-common-case=fetching_client_existing-endpoint=admin.json @@ -0,0 +1,27 @@ +{ + "body": { + "client_id": "existing-client", + "client_name": "", + "redirect_uris": [ + "http://localhost:3000/cb" + ], + "grant_types": [], + "response_types": [], + "scope": "offline_access offline openid", + "audience": [], + "owner": "", + "policy_uri": "", + "allowed_cors_origins": [], + "tos_uri": "", + "client_uri": "", + "logo_uri": "", + "contacts": [], + "client_secret_expires_at": 0, + "subject_type": "public", + "jwks": {}, + "token_endpoint_auth_method": "client_secret_basic", + "userinfo_signed_response_alg": "none", + "metadata": {} + }, + "status": 200 +} diff --git a/client/.snapshots/TestHandler-common-case=fetching_client_existing-endpoint=selfservice-with_correct_client_auth.json b/client/.snapshots/TestHandler-common-case=fetching_client_existing-endpoint=selfservice-with_correct_client_auth.json new file mode 100644 index 00000000000..56d8f4de4f3 --- /dev/null +++ b/client/.snapshots/TestHandler-common-case=fetching_client_existing-endpoint=selfservice-with_correct_client_auth.json @@ -0,0 +1,26 @@ +{ + "body": { + "client_id": "existing-client", + "client_name": "", + "redirect_uris": [ + "http://localhost:3000/cb" + ], + "grant_types": [], + "response_types": [], + "scope": "offline_access offline openid", + "audience": [], + "owner": "", + "policy_uri": "", + "allowed_cors_origins": [], + "tos_uri": "", + "client_uri": "", + "logo_uri": "", + "contacts": [], + "client_secret_expires_at": 0, + "subject_type": "public", + "jwks": {}, + "token_endpoint_auth_method": "client_secret_basic", + "userinfo_signed_response_alg": "none" + }, + "status": 200 +} diff --git a/client/.snapshots/TestHandler-common-case=fetching_client_which_does_not_exist-path=-clients-foo.json b/client/.snapshots/TestHandler-common-case=fetching_client_which_does_not_exist-path=-clients-foo.json new file mode 100644 index 00000000000..5b1c8352a82 --- /dev/null +++ b/client/.snapshots/TestHandler-common-case=fetching_client_which_does_not_exist-path=-clients-foo.json @@ -0,0 +1,7 @@ +{ + "body": { + "error": "Unable to locate the resource", + "error_description": "" + }, + "status": 404 +} diff --git a/client/.snapshots/TestHandler-common-case=fetching_client_which_does_not_exist-path=-connect-register?client_id=foo.json b/client/.snapshots/TestHandler-common-case=fetching_client_which_does_not_exist-path=-connect-register?client_id=foo.json new file mode 100644 index 00000000000..5c1758cd2c0 --- /dev/null +++ b/client/.snapshots/TestHandler-common-case=fetching_client_which_does_not_exist-path=-connect-register?client_id=foo.json @@ -0,0 +1,7 @@ +{ + "body": { + "error": "The request could not be authorized", + "error_description": "The requested OAuth 2.0 client does not exist or you did not provide the necessary credentials" + }, + "status": 401 +} diff --git a/client/doc.go b/client/doc.go index be75582f207..0fd37bf7acc 100644 --- a/client/doc.go +++ b/client/doc.go @@ -29,7 +29,7 @@ package client -// swagger:parameters createOAuth2Client +// swagger:parameters createOAuth2Client selfServiceCreateOAuth2Client type swaggerCreateClientPayload struct { // in: body // required: true @@ -47,11 +47,11 @@ type swaggerUpdateClientPayload struct { Body Client } -// swagger:parameters updateDynOAuth2Client +// swagger:parameters selfServiceUpdateOAuth2Client type swaggerUpdateDynClientPayload struct { - // in: path + // in: query // required: true - ID string `json:"id"` + ID string `json:"client_id"` // in: body // required: true @@ -115,10 +115,9 @@ type swaggerGetOAuth2Client struct { // swagger:parameters getDynOAuth2Client type swaggerGetDynOAuth2Client struct { - // The id of the OAuth 2.0 Client. - // - // in: path - ID string `json:"id"` + // in: query + // required: true + ID string `json:"client_id"` } // swagger:parameters deleteOAuth2Client @@ -129,10 +128,9 @@ type swaggerDeleteOAuth2Client struct { ID string `json:"id"` } -// swagger:parameters deleteDynOAuth2Client +// swagger:parameters selfServiceDeleteOAuth2Client type swaggerDeleteDynOAuth2Client struct { - // The id of the OAuth 2.0 Client. - // - // in: path - ID string `json:"id"` + // in: query + // required: true + ID string `json:"client_id"` } diff --git a/client/handler.go b/client/handler.go index 82b5c7ecfbd..9c60aad2c6d 100644 --- a/client/handler.go +++ b/client/handler.go @@ -27,11 +27,11 @@ import ( "net/http" "time" + "github.com/ory/fosite" + "github.com/ory/x/errorsx" "github.com/ory/herodot" - "github.com/ory/x/sqlcon" - "github.com/ory/hydra/x" "github.com/julienschmidt/httprouter" @@ -79,7 +79,6 @@ func (h *Handler) SetRoutes(admin *x.RouterAdmin, public *x.RouterPublic, dynami // // OAuth 2.0 clients are used to perform OAuth 2.0 and OpenID Connect flows. Usually, OAuth 2.0 clients are generated for applications which want to consume your OAuth 2.0 or OpenID Connect capabilities. To manage ORY Hydra, you will need an OAuth 2.0 Client as well. Make sure that this endpoint is well protected and only callable by first-party components. // -// // Consumes: // - application/json // @@ -90,9 +89,7 @@ func (h *Handler) SetRoutes(admin *x.RouterAdmin, public *x.RouterPublic, dynami // // Responses: // 201: oAuth2Client -// 400: jsonError -// 409: jsonError -// 500: jsonError +// default: jsonError func (h *Handler) Create(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { c, err := h.create(w, r, h.r.ClientValidator().Validate) if err != nil { @@ -126,9 +123,7 @@ func (h *Handler) Create(w http.ResponseWriter, r *http.Request, _ httprouter.Pa // // Responses: // 201: oAuth2Client -// 400: genericError -// 409: genericError -// 500: genericError +// default: jsonError func (h *Handler) CreateDynamicRegistration(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { c, err := h.create(w, r, h.r.ClientValidator().ValidateDynamicRegistration) if err != nil { @@ -139,7 +134,7 @@ func (h *Handler) CreateDynamicRegistration(w http.ResponseWriter, r *http.Reque h.r.Writer().WriteCreated(w, r, ClientsHandlerPath+"/"+c.GetID(), &c) } -func (h *Handler) create(w http.ResponseWriter, r *http.Request, f func(*Client) error) (*Client, error) { +func (h *Handler) create(w http.ResponseWriter, r *http.Request, validator func(*Client) error) (*Client, error) { var c Client if err := json.NewDecoder(r.Body).Decode(&c); err != nil { @@ -154,7 +149,7 @@ func (h *Handler) create(w http.ResponseWriter, r *http.Request, f func(*Client) c.Secret = string(secretb) } - if err := f(&c); err != nil { + if err := validator(&c); err != nil { return nil, err } @@ -189,7 +184,7 @@ func (h *Handler) create(w http.ResponseWriter, r *http.Request, f func(*Client) // // Responses: // 200: oAuth2Client -// 500: jsonError +// default: jsonError func (h *Handler) Update(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { var c Client @@ -207,62 +202,6 @@ func (h *Handler) Update(w http.ResponseWriter, r *http.Request, ps httprouter.P h.r.Writer().Write(w, r, &c) } -// swagger:route PATCH /clients/{id} admin patchOAuth2Client -// -// Patch an OAuth 2.0 Client -// -// Patch an existing OAuth 2.0 Client. If you pass `client_secret` the secret will be updated and returned via the API. This is the only time you will be able to retrieve the client secret, so write it down and keep it safe. -// -// OAuth 2.0 clients are used to perform OAuth 2.0 and OpenID Connect flows. Usually, OAuth 2.0 clients are generated for applications which want to consume your OAuth 2.0 or OpenID Connect capabilities. To manage ORY Hydra, you will need an OAuth 2.0 Client as well. Make sure that this endpoint is well protected and only callable by first-party components. -// -// Consumes: -// - application/json -// -// Produces: -// - application/json -// -// Schemes: http, https -// -// Responses: -// 200: oAuth2Client -// 500: jsonError -func (h *Handler) Patch(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { - patchJSON, err := io.ReadAll(r.Body) - if err != nil { - h.r.Writer().WriteError(w, r, err) - return - } - - id := ps.ByName("id") - c, err := h.r.ClientManager().GetConcreteClient(r.Context(), id) - if err != nil { - h.r.Writer().WriteError(w, r, err) - return - } - - oldSecret := c.Secret - - if err := x.ApplyJSONPatch(patchJSON, c, "/id"); err != nil { - h.r.Writer().WriteError(w, r, err) - return - } - - // fix for #2869 - // GetConcreteClient returns a client with the hashed secret, however updateClient expects - // an empty secret if the secret hasn't changed. As such we need to check if the patch has - // updated the secret or not - if oldSecret == c.Secret { - c.Secret = "" - } - - if err := h.updateClient(r.Context(), c); err != nil { - h.r.Writer().WriteError(w, r, err) - return - } - - h.r.Writer().Write(w, r, c) -} - func (h *Handler) updateClient(ctx context.Context, c *Client) error { var secret string if len(c.Secret) > 0 { @@ -280,7 +219,7 @@ func (h *Handler) updateClient(ctx context.Context, c *Client) error { return nil } -// swagger:route PUT/connect/register public selfServiceUpdateOAuth2Client +// swagger:route PUT /connect/register public selfServiceUpdateOAuth2Client // // Register an OAuth 2.0 Client using OpenID Dynamic Client Registration // @@ -301,7 +240,8 @@ func (h *Handler) updateClient(ctx context.Context, c *Client) error { // // Responses: // 200: oAuth2Client -// 500: genericError +// default: jsonError +// func (h *Handler) UpdateDynamicRegistration(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { client, err := h.r.ClientAuthenticator().AuthenticateClient(r.Context(), r, r.Form) if err != nil { @@ -311,6 +251,11 @@ func (h *Handler) UpdateDynamicRegistration(w http.ResponseWriter, r *http.Reque return } + if err := h.checkID(client, r); err != nil { + h.r.Writer().WriteError(w, r, err) + return + } + var c Client if err := json.NewDecoder(r.Body).Decode(&c); err != nil { h.r.Writer().WriteError(w, r, errorsx.WithStack(err)) @@ -338,6 +283,62 @@ func (h *Handler) UpdateDynamicRegistration(w http.ResponseWriter, r *http.Reque h.r.Writer().Write(w, r, &c) } +// swagger:route PATCH /clients/{id} admin patchOAuth2Client +// +// Patch an OAuth 2.0 Client +// +// Patch an existing OAuth 2.0 Client. If you pass `client_secret` the secret will be updated and returned via the API. This is the only time you will be able to retrieve the client secret, so write it down and keep it safe. +// +// OAuth 2.0 clients are used to perform OAuth 2.0 and OpenID Connect flows. Usually, OAuth 2.0 clients are generated for applications which want to consume your OAuth 2.0 or OpenID Connect capabilities. To manage ORY Hydra, you will need an OAuth 2.0 Client as well. Make sure that this endpoint is well protected and only callable by first-party components. +// +// Consumes: +// - application/json +// +// Produces: +// - application/json +// +// Schemes: http, https +// +// Responses: +// 200: oAuth2Client +// default: jsonError +func (h *Handler) Patch(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + patchJSON, err := io.ReadAll(r.Body) + if err != nil { + h.r.Writer().WriteError(w, r, err) + return + } + + id := ps.ByName("id") + c, err := h.r.ClientManager().GetConcreteClient(r.Context(), id) + if err != nil { + h.r.Writer().WriteError(w, r, err) + return + } + + oldSecret := c.Secret + + if err := x.ApplyJSONPatch(patchJSON, c, "/id"); err != nil { + h.r.Writer().WriteError(w, r, err) + return + } + + // fix for #2869 + // GetConcreteClient returns a client with the hashed secret, however updateClient expects + // an empty secret if the secret hasn't changed. As such we need to check if the patch has + // updated the secret or not + if oldSecret == c.Secret { + c.Secret = "" + } + + if err := h.updateClient(r.Context(), c); err != nil { + h.r.Writer().WriteError(w, r, err) + return + } + + h.r.Writer().Write(w, r, c) +} + // swagger:parameters listOAuth2Clients type Filter struct { // The maximum amount of clients to returned, upper bound is 500 clients. @@ -377,7 +378,7 @@ type Filter struct { // // Responses: // 200: oAuth2ClientList -// 500: jsonError +// default: jsonError func (h *Handler) List(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { limit, offset := pagination.Parse(r, 100, 0, 500) filters := Filter{ @@ -431,16 +432,12 @@ func (h *Handler) List(w http.ResponseWriter, r *http.Request, ps httprouter.Par // // Responses: // 200: oAuth2Client -// 401: jsonError -// 500: jsonError +// default: jsonError func (h *Handler) Get(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { var id = ps.ByName("id") c, err := h.r.ClientManager().GetConcreteClient(r.Context(), id) if err != nil { - if errors.Is(err, sqlcon.ErrNoRows) { - err = herodot.ErrUnauthorized.WithReason("The requested OAuth 2.0 client does not exist or you did not provide the necessary credentials") - } h.r.Writer().WriteError(w, r, err) return } @@ -468,8 +465,7 @@ func (h *Handler) Get(w http.ResponseWriter, r *http.Request, ps httprouter.Para // // Responses: // 200: oAuth2Client -// 401: genericError -// 500: genericError +// default: jsonError func (h *Handler) GetDynamicRegistration(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { client, err := h.r.ClientAuthenticator().AuthenticateClient(r.Context(), r, r.Form) if err != nil { @@ -479,6 +475,11 @@ func (h *Handler) GetDynamicRegistration(w http.ResponseWriter, r *http.Request, return } + if err := h.checkID(client, r); err != nil { + h.r.Writer().WriteError(w, r, err) + return + } + c, err := h.r.ClientManager().GetConcreteClient(r.Context(), client.GetID()) if err != nil { err = herodot.ErrUnauthorized.WithReason("The requested OAuth 2.0 client does not exist or you did not provide the necessary credentials") @@ -509,8 +510,7 @@ func (h *Handler) GetDynamicRegistration(w http.ResponseWriter, r *http.Request, // // Responses: // 204: emptyResponse -// 404: jsonError -// 500: jsonError +// default: jsonError func (h *Handler) Delete(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { var id = ps.ByName("id") if err := h.r.ClientManager().DeleteClient(r.Context(), id); err != nil { @@ -542,14 +542,18 @@ func (h *Handler) Delete(w http.ResponseWriter, r *http.Request, ps httprouter.P // // Responses: // 204: emptyResponse -// 404: genericError -// 500: genericError +// default: jsonError func (h *Handler) DeleteDynamicRegistration(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { client, err := h.r.ClientAuthenticator().AuthenticateClient(r.Context(), r, r.Form) if err != nil { h.r.Writer().WriteError(w, r, errors.WithStack(herodot.ErrUnauthorized. WithTrace(err). - WithReason("The requested OAuth 2.0 client does not exist or you did not provide the necessary credentials").WithDebug(err.Error()))) + WithReason("The requested OAuth 2.0 client does not exist or you did not provide the necessary credentials.").WithDebug(err.Error()))) + return + } + + if err := h.checkID(client, r); err != nil { + h.r.Writer().WriteError(w, r, err) return } @@ -560,3 +564,11 @@ func (h *Handler) DeleteDynamicRegistration(w http.ResponseWriter, r *http.Reque w.WriteHeader(http.StatusNoContent) } + +func (h *Handler) checkID(c fosite.Client, r *http.Request) error { + if r.URL.Query().Get("client_id") != c.GetID() { + return errors.WithStack(herodot.ErrUnauthorized.WithReason("The OAuth 2.0 Client ID from the path does not match the OAuth 2.0 Client used to authenticate the request.")) + } + + return nil +} diff --git a/client/handler_test.go b/client/handler_test.go index deddbadaf76..67bdc372100 100644 --- a/client/handler_test.go +++ b/client/handler_test.go @@ -1,207 +1,237 @@ -/* - * Copyright © 2015-2018 Javier Viera - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * @author Javier Viera - * @Copyright 2017-2018 Javier Viera - * @license Apache-2.0 - */ - package client_test import ( "bytes" + "encoding/json" + "fmt" + "io" "net/http" "net/http/httptest" "testing" - "github.com/stretchr/testify/require" + "github.com/julienschmidt/httprouter" + "github.com/stretchr/testify/assert" + + "github.com/ory/hydra/x" + "github.com/ory/x/snapshotx" - "github.com/ory/x/urlx" + "github.com/stretchr/testify/require" "github.com/ory/hydra/client" "github.com/ory/hydra/internal" ) -func TestValidateDynClientRegistrationAuthorizationBadReq(t *testing.T) { - reg := internal.NewMockedRegistry(t) - h := client.NewHandler(reg) - c := client.Client{OutfacingID: "someid"} - u := "https://www.something.com" - hr := &http.Request{Header: map[string][]string{}, URL: urlx.ParseOrPanic(u), RequestURI: u} - err := h.ValidateDynClientRegistrationAuthorization(hr, c) - require.EqualValues(t, "The request could not be authorized", err.Error()) -} - -func TestValidateDynClientRegistrationAuthorizationBadBasicAuthNoBase64(t *testing.T) { - reg := internal.NewMockedRegistry(t) - h := client.NewHandler(reg) - c := client.Client{OutfacingID: "someid"} - u := "https://www.something.com" - hr := &http.Request{Header: map[string][]string{}, URL: urlx.ParseOrPanic(u), RequestURI: u} - hr.Header.Add("Authorization", "Basic something") - err := h.ValidateDynClientRegistrationAuthorization(hr, c) - require.EqualValues(t, "The request could not be authorized", err.Error()) -} - -func TestValidateDynClientRegistrationAuthorizationBadBasicAuthKo(t *testing.T) { - reg := internal.NewMockedRegistry(t) - h := client.NewHandler(reg) - c := client.Client{OutfacingID: "client"} - u := "https://www.something.com" - hr := &http.Request{Header: map[string][]string{}, URL: urlx.ParseOrPanic(u), RequestURI: u} - hr.Header.Add("Authorization", "Basic Y2xpZW50OnNlY3JldA==") - err := h.ValidateDynClientRegistrationAuthorization(hr, c) - require.EqualValues(t, "The request could not be authorized", err.Error()) -} - -func TestCreateOk(t *testing.T) { - u := "https://www.something.com" - var jsonStr = []byte(`{"title":"Buy cheese and bread for breakfast."}`) - req, err := http.NewRequest("POST", u, bytes.NewBuffer(jsonStr)) - if err != nil { - panic(err) - } - rr := httptest.NewRecorder() - reg := internal.NewMockedRegistry(t) - h := client.NewHandler(reg) - h.Create(rr, req, nil) - require.EqualValues(t, http.StatusCreated, rr.Result().StatusCode) -} - -func TestCreateDynamicRegistrationOk(t *testing.T) { - u := "https://www.something.com" - var jsonStr = []byte(`{"title":"Buy cheese and bread for breakfast."}`) - req, err := http.NewRequest("POST", u, bytes.NewBuffer(jsonStr)) - if err != nil { - panic(err) - } - rr := httptest.NewRecorder() - reg := internal.NewMockedRegistry(t) - h := client.NewHandler(reg) - h.CreateDynamicRegistration(rr, req, nil) - require.EqualValues(t, http.StatusCreated, rr.Result().StatusCode) -} - -func TestUpdateKo(t *testing.T) { - u := "https://www.something.com" - var jsonStr = []byte(`{"title":"Buy cheese and bread for breakfast."}`) - req, err := http.NewRequest("POST", u, bytes.NewBuffer(jsonStr)) - if err != nil { - panic(err) - } - rr := httptest.NewRecorder() - reg := internal.NewMockedRegistry(t) - h := client.NewHandler(reg) - h.Update(rr, req, nil) - require.EqualValues(t, http.StatusNotFound, rr.Result().StatusCode) +type responseSnapshot struct { + Body json.RawMessage `json:"body"` + Status int `json:"status"` } -func TestPatchKo(t *testing.T) { - u := "https://www.something.com" - var jsonStr = []byte(`{"title":"Buy cheese and bread for breakfast."}`) - req, err := http.NewRequest("POST", u, bytes.NewBuffer(jsonStr)) - if err != nil { - panic(err) +func newResponseSnapshot(body string, res *http.Response) *responseSnapshot { + return &responseSnapshot{ + Body: json.RawMessage(body), + Status: res.StatusCode, } - rr := httptest.NewRecorder() - reg := internal.NewMockedRegistry(t) - h := client.NewHandler(reg) - h.Patch(rr, req, nil) - require.EqualValues(t, http.StatusNotFound, rr.Result().StatusCode) } -func TestUpdateDynamicRegistrationKo(t *testing.T) { - u := "https://www.something.com" - var jsonStr = []byte(`{"title":"Buy cheese and bread for breakfast."}`) - req, err := http.NewRequest("POST", u, bytes.NewBuffer(jsonStr)) - if err != nil { - panic(err) - } - rr := httptest.NewRecorder() +func TestHandler(t *testing.T) { reg := internal.NewMockedRegistry(t) h := client.NewHandler(reg) - h.UpdateDynamicRegistration(rr, req, nil) - require.EqualValues(t, http.StatusUnauthorized, rr.Result().StatusCode) -} -func TestListOk(t *testing.T) { - u := "https://www.something.com" - var jsonStr = []byte(`{"title":"Buy cheese and bread for breakfast."}`) - req, err := http.NewRequest("POST", u, bytes.NewBuffer(jsonStr)) - if err != nil { - panic(err) + newServer := func(t *testing.T, dynamicEnabled bool) (*httptest.Server, *http.Client) { + router := httprouter.New() + h.SetRoutes(&x.RouterAdmin{Router: router}, &x.RouterPublic{Router: router}, dynamicEnabled) + ts := httptest.NewServer(router) + t.Cleanup(ts.Close) + return ts, ts.Client() } - rr := httptest.NewRecorder() - reg := internal.NewMockedRegistry(t) - h := client.NewHandler(reg) - h.List(rr, req, nil) - require.EqualValues(t, http.StatusOK, rr.Result().StatusCode) -} -func TestGetKo(t *testing.T) { - u := "https://www.something.com" - var jsonStr = []byte(`{"title":"Buy cheese and bread for breakfast."}`) - req, err := http.NewRequest("POST", u, bytes.NewBuffer(jsonStr)) - if err != nil { - panic(err) + fetch := func(t *testing.T, url string) (string, *http.Response) { + res, err := http.Get(url) + require.NoError(t, err) + defer res.Body.Close() + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + return string(body), res } - rr := httptest.NewRecorder() - reg := internal.NewMockedRegistry(t) - h := client.NewHandler(reg) - h.Get(rr, req, nil) - require.EqualValues(t, http.StatusUnauthorized, rr.Result().StatusCode) -} -func TestGetDynamicRegistrationKo(t *testing.T) { - u := "https://www.something.com" - var jsonStr = []byte(`{"title":"Buy cheese and bread for breakfast."}`) - req, err := http.NewRequest("POST", u, bytes.NewBuffer(jsonStr)) - if err != nil { - panic(err) + fetchWithAuth := func(t *testing.T, method, url, username, password string, body io.Reader) (string, *http.Response) { + r, err := http.NewRequest(method, url, body) + require.NoError(t, err) + r.SetBasicAuth(username, password) + res, err := http.DefaultClient.Do(r) + require.NoError(t, err) + defer res.Body.Close() + out, err := io.ReadAll(res.Body) + require.NoError(t, err) + return string(out), res } - rr := httptest.NewRecorder() - reg := internal.NewMockedRegistry(t) - h := client.NewHandler(reg) - h.GetDynamicRegistration(rr, req, nil) - require.EqualValues(t, http.StatusUnauthorized, rr.Result().StatusCode) -} -func TestDeleteKo(t *testing.T) { - u := "https://www.something.com" - var jsonStr = []byte(`{"title":"Buy cheese and bread for breakfast."}`) - req, err := http.NewRequest("POST", u, bytes.NewBuffer(jsonStr)) - if err != nil { - panic(err) + makeJSON := func(t *testing.T, ts *httptest.Server, method string, path string, body interface{}) (string, *http.Response) { + var b bytes.Buffer + require.NoError(t, json.NewEncoder(&b).Encode(body)) + r, err := http.NewRequest(method, ts.URL+path, &b) + require.NoError(t, err) + r.Header.Set("Content-Type", "application/json") + res, err := ts.Client().Do(r) + require.NoError(t, err) + defer res.Body.Close() + rb, err := io.ReadAll(res.Body) + require.NoError(t, err) + return string(rb), res } - rr := httptest.NewRecorder() - reg := internal.NewMockedRegistry(t) - h := client.NewHandler(reg) - h.Delete(rr, req, nil) - require.EqualValues(t, http.StatusNotFound, rr.Result().StatusCode) -} -func TestDeleteDynamicRegistrationKo(t *testing.T) { - u := "https://www.something.com" - var jsonStr = []byte(`{"title":"Buy cheese and bread for breakfast."}`) - req, err := http.NewRequest("POST", u, bytes.NewBuffer(jsonStr)) - if err != nil { - panic(err) - } - rr := httptest.NewRecorder() - reg := internal.NewMockedRegistry(t) - h := client.NewHandler(reg) - h.DeleteDynamicRegistration(rr, req, nil) - require.EqualValues(t, http.StatusUnauthorized, rr.Result().StatusCode) + t.Run("selfservice disabled", func(t *testing.T) { + ts, hc := newServer(t, false) + + for _, method := range []string{"GET", "POST", "PUT", "DELETE"} { + t.Run("method="+method, func(t *testing.T) { + req, err := http.NewRequest(method, ts.URL+client.DynClientsHandlerPath, nil) + require.NoError(t, err) + + res, err := hc.Do(req) + require.NoError(t, err) + require.Equal(t, http.StatusNotFound, res.StatusCode) + }) + } + }) + + t.Run("case=selfservice with incorrect or missing auth", func(t *testing.T) { + ts, hc := newServer(t, true) + expected := &client.Client{ + OutfacingID: "incorrect-missing-client", + Secret: "averylongsecret", + RedirectURIs: []string{"http://localhost:3000/cb"}, + TokenEndpointAuthMethod: "client_secret_basic", + } + body, res := makeJSON(t, ts, "POST", client.ClientsHandlerPath, expected) + require.Equal(t, http.StatusCreated, res.StatusCode, body) + + // Create the second client + secondClient := &client.Client{ + OutfacingID: "second-existing-client", + Secret: "averylongsecret", + RedirectURIs: []string{"http://localhost:3000/cb"}, + } + body, res = makeJSON(t, ts, "POST", client.ClientsHandlerPath, secondClient) + require.Equal(t, http.StatusCreated, res.StatusCode, body) + + t.Run("endpoint=selfservice", func(t *testing.T) { + for _, method := range []string{"GET", "DELETE", "PUT"} { + t.Run("method="+method, func(t *testing.T) { + t.Run("without auth", func(t *testing.T) { + req, err := http.NewRequest(method, ts.URL+client.DynClientsHandlerPath+"?client_id="+expected.OutfacingID, nil) + require.NoError(t, err) + + res, err := hc.Do(req) + require.NoError(t, err) + defer res.Body.Close() + + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + + snapshotx.SnapshotTExcept(t, newResponseSnapshot(string(body), res), nil) + }) + + t.Run("without incorrect auth", func(t *testing.T) { + body, res := fetchWithAuth(t, method, ts.URL+client.DynClientsHandlerPath+"?client_id="+expected.OutfacingID, expected.OutfacingID, "incorrect", nil) + assert.Equal(t, http.StatusUnauthorized, res.StatusCode) + snapshotx.SnapshotTExcept(t, newResponseSnapshot(body, res), nil) + }) + + t.Run("with a different client auth", func(t *testing.T) { + body, res = fetchWithAuth(t, method, ts.URL+client.DynClientsHandlerPath+"?client_id="+expected.OutfacingID, secondClient.OutfacingID, secondClient.Secret, nil) + assert.Equal(t, http.StatusUnauthorized, res.StatusCode) + snapshotx.SnapshotTExcept(t, newResponseSnapshot(body, res), nil) + }) + }) + } + }) + }) + + t.Run("common", func(t *testing.T) { + ts, _ := newServer(t, true) + t.Run("case=create clients", func(t *testing.T) { + for k, tc := range []struct { + d string + payload *client.Client + path string + statusCode int + }{ + { + d: "standard", + payload: &client.Client{ + OutfacingID: "standard-client", + Secret: "averylongsecret", + RedirectURIs: []string{"http://localhost:3000/cb"}, + }, + path: client.DynClientsHandlerPath, + statusCode: http.StatusCreated, + }, + { + d: "metadata fails", + payload: &client.Client{ + OutfacingID: "standard-client", + Secret: "averylongsecret", + RedirectURIs: []string{"http://localhost:3000/cb"}, + Metadata: []byte(`{"foo":"bar"}`), + }, + path: client.DynClientsHandlerPath, + statusCode: http.StatusBadRequest, + }, + { + d: "short secret fails", + payload: &client.Client{ + OutfacingID: "standard-client", + Secret: "short", + RedirectURIs: []string{"http://localhost:3000/cb"}, + }, + path: client.DynClientsHandlerPath, + statusCode: http.StatusBadRequest, + }, + } { + t.Run(fmt.Sprintf("case=%d/description=%s", k, tc.d), func(t *testing.T) { + body, res := makeJSON(t, ts, "POST", tc.path, tc.payload) + require.Equal(t, tc.statusCode, res.StatusCode, body) + snapshotx.SnapshotTExcept(t, json.RawMessage(body), []string{"updated_at", "created_at"}) + }) + } + }) + + t.Run("case=fetching client which does not exist", func(t *testing.T) { + for _, path := range []string{ + client.DynClientsHandlerPath + "?client_id=foo", + client.ClientsHandlerPath + "/foo", + } { + t.Run("path="+path, func(t *testing.T) { + body, res := fetch(t, ts.URL+path) + snapshotx.SnapshotTExcept(t, newResponseSnapshot(body, res), nil) + }) + } + }) + + t.Run("case=fetching client existing", func(t *testing.T) { + expected := &client.Client{ + OutfacingID: "existing-client", + Secret: "averylongsecret", + RedirectURIs: []string{"http://localhost:3000/cb"}, + TokenEndpointAuthMethod: "client_secret_basic", + } + body, res := makeJSON(t, ts, "POST", client.ClientsHandlerPath, expected) + require.Equal(t, http.StatusCreated, res.StatusCode, body) + + t.Run("endpoint=admin", func(t *testing.T) { + body, res := fetch(t, ts.URL+client.ClientsHandlerPath+"/"+expected.OutfacingID) + assert.Equal(t, http.StatusOK, res.StatusCode) + snapshotx.SnapshotTExcept(t, newResponseSnapshot(body, res), []string{"body.created_at", "body.updated_at"}) + }) + + t.Run("endpoint=selfservice", func(t *testing.T) { + t.Run("with correct client auth", func(t *testing.T) { + body, res = fetchWithAuth(t, "GET", ts.URL+client.DynClientsHandlerPath+"?client_id="+expected.OutfacingID, expected.OutfacingID, expected.Secret, nil) + assert.Equal(t, http.StatusOK, res.StatusCode) + snapshotx.SnapshotTExcept(t, newResponseSnapshot(body, res), []string{"body.created_at", "body.updated_at"}) + }) + }) + }) + }) } diff --git a/driver/registry_base.go b/driver/registry_base.go index 2542384120c..65ab12bfb43 100644 --- a/driver/registry_base.go +++ b/driver/registry_base.go @@ -501,3 +501,7 @@ func (m *RegistryBase) AccessRequestHooks() []oauth2.AccessRequestHook { } return m.arhs } + +func (m *RegistrySQL) ClientAuthenticator() x.ClientAuthenticator { + return m.OAuth2Provider().(*fosite.Fosite) +} diff --git a/x/authenticator.go b/x/authenticator.go index 1c333fd702c..62e441c232d 100644 --- a/x/authenticator.go +++ b/x/authenticator.go @@ -2,9 +2,10 @@ package x import ( "context" - "github.com/ory/fosite" "net/http" "net/url" + + "github.com/ory/fosite" ) type ClientAuthenticatorProvider interface {