├── .github └── workflows │ └── ci.yml ├── .gitignore ├── Caddyfile ├── Dockerfile ├── LICENSE.md ├── Makefile ├── README.md ├── composer.json ├── database └── migrations │ ├── 2018_05_11_203533_create_open_i_d_providers_table.php │ ├── 2020_01_01_000001_create_oidc_clients_table.php │ └── 2020_01_02_000001_create_oidc_personal_access_clients_table.php ├── docker-compose.yml ├── examples ├── App │ ├── Providers │ │ └── AuthServiceProvider.php │ └── User.php └── config │ └── auth.php ├── phpunit.xml ├── src ├── Bridge │ ├── AccessToken.php │ ├── AccessTokenRepository.php │ ├── ClaimEntity.php │ ├── ClaimRepository.php │ ├── ClientRepository.php │ └── UserRepository.php ├── ClientController.php ├── ClientRepository.php ├── Guards │ └── TokenGuard.php ├── Http │ └── Controllers │ │ ├── AccessTokenController.php │ │ └── AuthorizationController.php ├── IntrospectionController.php ├── KeyRepository.php ├── Middleware │ └── AllowClientCORS.php ├── Model │ ├── Client.php │ ├── PersonalAccessClient.php │ ├── Provider.php │ └── ProviderInterface.php ├── Passport.php ├── PassportConfig.php ├── PassportServiceProvider.php ├── ProviderController.php ├── ProviderRepository.php ├── ProviderRepositoryInterface.php ├── RevokeController.php ├── RouteRegistrar.php ├── SessionManagementController.php ├── TokenCache.php ├── UserInfoController.php └── routes │ └── web.php └── tests ├── AccessTokenEntity.php ├── AuthorizationControllerTest.php ├── Bootstrap.php ├── UserinfoControllerTest.php └── files ├── .gitkeep ├── oauth-private.key └── oauth-public.key /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build-test: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v1 11 | - uses: php-actions/composer@v6 12 | with: 13 | php_version: 8.1 14 | - uses: php-actions/phpunit@v3 15 | with: 16 | configuration: phpunit.xml 17 | php_version: 8.1 18 | version: 8.5 19 | php_extensions: xdebug 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | vendor 3 | .phpunit.result.cache 4 | tests/files/oauth-public.key 5 | tests/files/oauth-private.key 6 | .vscode 7 | composer.lock 8 | .idea 9 | -------------------------------------------------------------------------------- /Caddyfile: -------------------------------------------------------------------------------- 1 | :1443 2 | 3 | reverse_proxy laravel-openid-connect:8000 4 | 5 | tls /docker/openid-connect.test.crt /docker/openid-connect.test.key 6 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM php:7.4-alpine 2 | 3 | RUN apk add --no-cache git jq moreutils yarn 4 | RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer 5 | RUN composer create-project --prefer-dist laravel/laravel example && cd example 6 | 7 | WORKDIR /example 8 | 9 | RUN composer require moontoast/math laravel/ui 10 | 11 | RUN cd /src; php artisan ui vue --auth && \ 12 | yarn install && \ 13 | yarn production 14 | 15 | COPY . /laravel-openid-connect 16 | RUN jq '.repositories=[{"type": "path","url": "/laravel-openid-connect"}]' ./composer.json | sponge ./composer.json 17 | 18 | RUN composer require nl.idaas/laravel-openid-connect @dev 19 | 20 | RUN touch ./.database.sqlite && \ 21 | echo "DB_CONNECTION=sqlite" >> ./.env && \ 22 | echo "DB_DATABASE=/example/.database.sqlite" >> ./.env 23 | 24 | RUN php artisan migrate 25 | RUN php artisan passport:install 26 | RUN php artisan vendor:publish --provider="Idaas\Passport\PassportServiceProvider" --force 27 | 28 | # php artisan passport:client --user_id=0 --name=op-test --redirect_uri=https://op-test:60001/authz_cb 29 | 30 | CMD php artisan serve --host=0.0.0.0 --port=8000 31 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | ### GNU AFFERO GENERAL PUBLIC LICENSE 2 | 3 | Version 3, 19 November 2007 4 | 5 | Copyright (C) 2007 Free Software Foundation, Inc. 6 | 7 | 8 | Everyone is permitted to copy and distribute verbatim copies of this 9 | license document, but changing it is not allowed. 10 | 11 | ### Preamble 12 | 13 | The GNU Affero General Public License is a free, copyleft license for 14 | software and other kinds of works, specifically designed to ensure 15 | cooperation with the community in the case of network server software. 16 | 17 | The licenses for most software and other practical works are designed 18 | to take away your freedom to share and change the works. By contrast, 19 | our General Public Licenses are intended to guarantee your freedom to 20 | share and change all versions of a program--to make sure it remains 21 | free software for all its users. 22 | 23 | When we speak of free software, we are referring to freedom, not 24 | price. Our General Public Licenses are designed to make sure that you 25 | have the freedom to distribute copies of free software (and charge for 26 | them if you wish), that you receive source code or can get it if you 27 | want it, that you can change the software or use pieces of it in new 28 | free programs, and that you know you can do these things. 29 | 30 | Developers that use our General Public Licenses protect your rights 31 | with two steps: (1) assert copyright on the software, and (2) offer 32 | you this License which gives you legal permission to copy, distribute 33 | and/or modify the software. 34 | 35 | A secondary benefit of defending all users' freedom is that 36 | improvements made in alternate versions of the program, if they 37 | receive widespread use, become available for other developers to 38 | incorporate. Many developers of free software are heartened and 39 | encouraged by the resulting cooperation. However, in the case of 40 | software used on network servers, this result may fail to come about. 41 | The GNU General Public License permits making a modified version and 42 | letting the public access it on a server without ever releasing its 43 | source code to the public. 44 | 45 | The GNU Affero General Public License is designed specifically to 46 | ensure that, in such cases, the modified source code becomes available 47 | to the community. It requires the operator of a network server to 48 | provide the source code of the modified version running there to the 49 | users of that server. Therefore, public use of a modified version, on 50 | a publicly accessible server, gives the public access to the source 51 | code of the modified version. 52 | 53 | An older license, called the Affero General Public License and 54 | published by Affero, was designed to accomplish similar goals. This is 55 | a different license, not a version of the Affero GPL, but Affero has 56 | released a new version of the Affero GPL which permits relicensing 57 | under this license. 58 | 59 | The precise terms and conditions for copying, distribution and 60 | modification follow. 61 | 62 | ### TERMS AND CONDITIONS 63 | 64 | #### 0. Definitions. 65 | 66 | "This License" refers to version 3 of the GNU Affero General Public 67 | License. 68 | 69 | "Copyright" also means copyright-like laws that apply to other kinds 70 | of works, such as semiconductor masks. 71 | 72 | "The Program" refers to any copyrightable work licensed under this 73 | License. Each licensee is addressed as "you". "Licensees" and 74 | "recipients" may be individuals or organizations. 75 | 76 | To "modify" a work means to copy from or adapt all or part of the work 77 | in a fashion requiring copyright permission, other than the making of 78 | an exact copy. The resulting work is called a "modified version" of 79 | the earlier work or a work "based on" the earlier work. 80 | 81 | A "covered work" means either the unmodified Program or a work based 82 | on the Program. 83 | 84 | To "propagate" a work means to do anything with it that, without 85 | permission, would make you directly or secondarily liable for 86 | infringement under applicable copyright law, except executing it on a 87 | computer or modifying a private copy. Propagation includes copying, 88 | distribution (with or without modification), making available to the 89 | public, and in some countries other activities as well. 90 | 91 | To "convey" a work means any kind of propagation that enables other 92 | parties to make or receive copies. Mere interaction with a user 93 | through a computer network, with no transfer of a copy, is not 94 | conveying. 95 | 96 | An interactive user interface displays "Appropriate Legal Notices" to 97 | the extent that it includes a convenient and prominently visible 98 | feature that (1) displays an appropriate copyright notice, and (2) 99 | tells the user that there is no warranty for the work (except to the 100 | extent that warranties are provided), that licensees may convey the 101 | work under this License, and how to view a copy of this License. If 102 | the interface presents a list of user commands or options, such as a 103 | menu, a prominent item in the list meets this criterion. 104 | 105 | #### 1. Source Code. 106 | 107 | The "source code" for a work means the preferred form of the work for 108 | making modifications to it. "Object code" means any non-source form of 109 | a work. 110 | 111 | A "Standard Interface" means an interface that either is an official 112 | standard defined by a recognized standards body, or, in the case of 113 | interfaces specified for a particular programming language, one that 114 | is widely used among developers working in that language. 115 | 116 | The "System Libraries" of an executable work include anything, other 117 | than the work as a whole, that (a) is included in the normal form of 118 | packaging a Major Component, but which is not part of that Major 119 | Component, and (b) serves only to enable use of the work with that 120 | Major Component, or to implement a Standard Interface for which an 121 | implementation is available to the public in source code form. A 122 | "Major Component", in this context, means a major essential component 123 | (kernel, window system, and so on) of the specific operating system 124 | (if any) on which the executable work runs, or a compiler used to 125 | produce the work, or an object code interpreter used to run it. 126 | 127 | The "Corresponding Source" for a work in object code form means all 128 | the source code needed to generate, install, and (for an executable 129 | work) run the object code and to modify the work, including scripts to 130 | control those activities. However, it does not include the work's 131 | System Libraries, or general-purpose tools or generally available free 132 | programs which are used unmodified in performing those activities but 133 | which are not part of the work. For example, Corresponding Source 134 | includes interface definition files associated with source files for 135 | the work, and the source code for shared libraries and dynamically 136 | linked subprograms that the work is specifically designed to require, 137 | such as by intimate data communication or control flow between those 138 | subprograms and other parts of the work. 139 | 140 | The Corresponding Source need not include anything that users can 141 | regenerate automatically from other parts of the Corresponding Source. 142 | 143 | The Corresponding Source for a work in source code form is that same 144 | work. 145 | 146 | #### 2. Basic Permissions. 147 | 148 | All rights granted under this License are granted for the term of 149 | copyright on the Program, and are irrevocable provided the stated 150 | conditions are met. This License explicitly affirms your unlimited 151 | permission to run the unmodified Program. The output from running a 152 | covered work is covered by this License only if the output, given its 153 | content, constitutes a covered work. This License acknowledges your 154 | rights of fair use or other equivalent, as provided by copyright law. 155 | 156 | You may make, run and propagate covered works that you do not convey, 157 | without conditions so long as your license otherwise remains in force. 158 | You may convey covered works to others for the sole purpose of having 159 | them make modifications exclusively for you, or provide you with 160 | facilities for running those works, provided that you comply with the 161 | terms of this License in conveying all material for which you do not 162 | control copyright. Those thus making or running the covered works for 163 | you must do so exclusively on your behalf, under your direction and 164 | control, on terms that prohibit them from making any copies of your 165 | copyrighted material outside their relationship with you. 166 | 167 | Conveying under any other circumstances is permitted solely under the 168 | conditions stated below. Sublicensing is not allowed; section 10 makes 169 | it unnecessary. 170 | 171 | #### 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 172 | 173 | No covered work shall be deemed part of an effective technological 174 | measure under any applicable law fulfilling obligations under article 175 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 176 | similar laws prohibiting or restricting circumvention of such 177 | measures. 178 | 179 | When you convey a covered work, you waive any legal power to forbid 180 | circumvention of technological measures to the extent such 181 | circumvention is effected by exercising rights under this License with 182 | respect to the covered work, and you disclaim any intention to limit 183 | operation or modification of the work as a means of enforcing, against 184 | the work's users, your or third parties' legal rights to forbid 185 | circumvention of technological measures. 186 | 187 | #### 4. Conveying Verbatim Copies. 188 | 189 | You may convey verbatim copies of the Program's source code as you 190 | receive it, in any medium, provided that you conspicuously and 191 | appropriately publish on each copy an appropriate copyright notice; 192 | keep intact all notices stating that this License and any 193 | non-permissive terms added in accord with section 7 apply to the code; 194 | keep intact all notices of the absence of any warranty; and give all 195 | recipients a copy of this License along with the Program. 196 | 197 | You may charge any price or no price for each copy that you convey, 198 | and you may offer support or warranty protection for a fee. 199 | 200 | #### 5. Conveying Modified Source Versions. 201 | 202 | You may convey a work based on the Program, or the modifications to 203 | produce it from the Program, in the form of source code under the 204 | terms of section 4, provided that you also meet all of these 205 | conditions: 206 | 207 | - a) The work must carry prominent notices stating that you modified 208 | it, and giving a relevant date. 209 | - b) The work must carry prominent notices stating that it is 210 | released under this License and any conditions added under 211 | section 7. This requirement modifies the requirement in section 4 212 | to "keep intact all notices". 213 | - c) You must license the entire work, as a whole, under this 214 | License to anyone who comes into possession of a copy. This 215 | License will therefore apply, along with any applicable section 7 216 | additional terms, to the whole of the work, and all its parts, 217 | regardless of how they are packaged. This License gives no 218 | permission to license the work in any other way, but it does not 219 | invalidate such permission if you have separately received it. 220 | - d) If the work has interactive user interfaces, each must display 221 | Appropriate Legal Notices; however, if the Program has interactive 222 | interfaces that do not display Appropriate Legal Notices, your 223 | work need not make them do so. 224 | 225 | A compilation of a covered work with other separate and independent 226 | works, which are not by their nature extensions of the covered work, 227 | and which are not combined with it such as to form a larger program, 228 | in or on a volume of a storage or distribution medium, is called an 229 | "aggregate" if the compilation and its resulting copyright are not 230 | used to limit the access or legal rights of the compilation's users 231 | beyond what the individual works permit. Inclusion of a covered work 232 | in an aggregate does not cause this License to apply to the other 233 | parts of the aggregate. 234 | 235 | #### 6. Conveying Non-Source Forms. 236 | 237 | You may convey a covered work in object code form under the terms of 238 | sections 4 and 5, provided that you also convey the machine-readable 239 | Corresponding Source under the terms of this License, in one of these 240 | ways: 241 | 242 | - a) Convey the object code in, or embodied in, a physical product 243 | (including a physical distribution medium), accompanied by the 244 | Corresponding Source fixed on a durable physical medium 245 | customarily used for software interchange. 246 | - b) Convey the object code in, or embodied in, a physical product 247 | (including a physical distribution medium), accompanied by a 248 | written offer, valid for at least three years and valid for as 249 | long as you offer spare parts or customer support for that product 250 | model, to give anyone who possesses the object code either (1) a 251 | copy of the Corresponding Source for all the software in the 252 | product that is covered by this License, on a durable physical 253 | medium customarily used for software interchange, for a price no 254 | more than your reasonable cost of physically performing this 255 | conveying of source, or (2) access to copy the Corresponding 256 | Source from a network server at no charge. 257 | - c) Convey individual copies of the object code with a copy of the 258 | written offer to provide the Corresponding Source. This 259 | alternative is allowed only occasionally and noncommercially, and 260 | only if you received the object code with such an offer, in accord 261 | with subsection 6b. 262 | - d) Convey the object code by offering access from a designated 263 | place (gratis or for a charge), and offer equivalent access to the 264 | Corresponding Source in the same way through the same place at no 265 | further charge. You need not require recipients to copy the 266 | Corresponding Source along with the object code. If the place to 267 | copy the object code is a network server, the Corresponding Source 268 | may be on a different server (operated by you or a third party) 269 | that supports equivalent copying facilities, provided you maintain 270 | clear directions next to the object code saying where to find the 271 | Corresponding Source. Regardless of what server hosts the 272 | Corresponding Source, you remain obligated to ensure that it is 273 | available for as long as needed to satisfy these requirements. 274 | - e) Convey the object code using peer-to-peer transmission, 275 | provided you inform other peers where the object code and 276 | Corresponding Source of the work are being offered to the general 277 | public at no charge under subsection 6d. 278 | 279 | A separable portion of the object code, whose source code is excluded 280 | from the Corresponding Source as a System Library, need not be 281 | included in conveying the object code work. 282 | 283 | A "User Product" is either (1) a "consumer product", which means any 284 | tangible personal property which is normally used for personal, 285 | family, or household purposes, or (2) anything designed or sold for 286 | incorporation into a dwelling. In determining whether a product is a 287 | consumer product, doubtful cases shall be resolved in favor of 288 | coverage. For a particular product received by a particular user, 289 | "normally used" refers to a typical or common use of that class of 290 | product, regardless of the status of the particular user or of the way 291 | in which the particular user actually uses, or expects or is expected 292 | to use, the product. A product is a consumer product regardless of 293 | whether the product has substantial commercial, industrial or 294 | non-consumer uses, unless such uses represent the only significant 295 | mode of use of the product. 296 | 297 | "Installation Information" for a User Product means any methods, 298 | procedures, authorization keys, or other information required to 299 | install and execute modified versions of a covered work in that User 300 | Product from a modified version of its Corresponding Source. The 301 | information must suffice to ensure that the continued functioning of 302 | the modified object code is in no case prevented or interfered with 303 | solely because modification has been made. 304 | 305 | If you convey an object code work under this section in, or with, or 306 | specifically for use in, a User Product, and the conveying occurs as 307 | part of a transaction in which the right of possession and use of the 308 | User Product is transferred to the recipient in perpetuity or for a 309 | fixed term (regardless of how the transaction is characterized), the 310 | Corresponding Source conveyed under this section must be accompanied 311 | by the Installation Information. But this requirement does not apply 312 | if neither you nor any third party retains the ability to install 313 | modified object code on the User Product (for example, the work has 314 | been installed in ROM). 315 | 316 | The requirement to provide Installation Information does not include a 317 | requirement to continue to provide support service, warranty, or 318 | updates for a work that has been modified or installed by the 319 | recipient, or for the User Product in which it has been modified or 320 | installed. Access to a network may be denied when the modification 321 | itself materially and adversely affects the operation of the network 322 | or violates the rules and protocols for communication across the 323 | network. 324 | 325 | Corresponding Source conveyed, and Installation Information provided, 326 | in accord with this section must be in a format that is publicly 327 | documented (and with an implementation available to the public in 328 | source code form), and must require no special password or key for 329 | unpacking, reading or copying. 330 | 331 | #### 7. Additional Terms. 332 | 333 | "Additional permissions" are terms that supplement the terms of this 334 | License by making exceptions from one or more of its conditions. 335 | Additional permissions that are applicable to the entire Program shall 336 | be treated as though they were included in this License, to the extent 337 | that they are valid under applicable law. If additional permissions 338 | apply only to part of the Program, that part may be used separately 339 | under those permissions, but the entire Program remains governed by 340 | this License without regard to the additional permissions. 341 | 342 | When you convey a copy of a covered work, you may at your option 343 | remove any additional permissions from that copy, or from any part of 344 | it. (Additional permissions may be written to require their own 345 | removal in certain cases when you modify the work.) You may place 346 | additional permissions on material, added by you to a covered work, 347 | for which you have or can give appropriate copyright permission. 348 | 349 | Notwithstanding any other provision of this License, for material you 350 | add to a covered work, you may (if authorized by the copyright holders 351 | of that material) supplement the terms of this License with terms: 352 | 353 | - a) Disclaiming warranty or limiting liability differently from the 354 | terms of sections 15 and 16 of this License; or 355 | - b) Requiring preservation of specified reasonable legal notices or 356 | author attributions in that material or in the Appropriate Legal 357 | Notices displayed by works containing it; or 358 | - c) Prohibiting misrepresentation of the origin of that material, 359 | or requiring that modified versions of such material be marked in 360 | reasonable ways as different from the original version; or 361 | - d) Limiting the use for publicity purposes of names of licensors 362 | or authors of the material; or 363 | - e) Declining to grant rights under trademark law for use of some 364 | trade names, trademarks, or service marks; or 365 | - f) Requiring indemnification of licensors and authors of that 366 | material by anyone who conveys the material (or modified versions 367 | of it) with contractual assumptions of liability to the recipient, 368 | for any liability that these contractual assumptions directly 369 | impose on those licensors and authors. 370 | 371 | All other non-permissive additional terms are considered "further 372 | restrictions" within the meaning of section 10. If the Program as you 373 | received it, or any part of it, contains a notice stating that it is 374 | governed by this License along with a term that is a further 375 | restriction, you may remove that term. If a license document contains 376 | a further restriction but permits relicensing or conveying under this 377 | License, you may add to a covered work material governed by the terms 378 | of that license document, provided that the further restriction does 379 | not survive such relicensing or conveying. 380 | 381 | If you add terms to a covered work in accord with this section, you 382 | must place, in the relevant source files, a statement of the 383 | additional terms that apply to those files, or a notice indicating 384 | where to find the applicable terms. 385 | 386 | Additional terms, permissive or non-permissive, may be stated in the 387 | form of a separately written license, or stated as exceptions; the 388 | above requirements apply either way. 389 | 390 | #### 8. Termination. 391 | 392 | You may not propagate or modify a covered work except as expressly 393 | provided under this License. Any attempt otherwise to propagate or 394 | modify it is void, and will automatically terminate your rights under 395 | this License (including any patent licenses granted under the third 396 | paragraph of section 11). 397 | 398 | However, if you cease all violation of this License, then your license 399 | from a particular copyright holder is reinstated (a) provisionally, 400 | unless and until the copyright holder explicitly and finally 401 | terminates your license, and (b) permanently, if the copyright holder 402 | fails to notify you of the violation by some reasonable means prior to 403 | 60 days after the cessation. 404 | 405 | Moreover, your license from a particular copyright holder is 406 | reinstated permanently if the copyright holder notifies you of the 407 | violation by some reasonable means, this is the first time you have 408 | received notice of violation of this License (for any work) from that 409 | copyright holder, and you cure the violation prior to 30 days after 410 | your receipt of the notice. 411 | 412 | Termination of your rights under this section does not terminate the 413 | licenses of parties who have received copies or rights from you under 414 | this License. If your rights have been terminated and not permanently 415 | reinstated, you do not qualify to receive new licenses for the same 416 | material under section 10. 417 | 418 | #### 9. Acceptance Not Required for Having Copies. 419 | 420 | You are not required to accept this License in order to receive or run 421 | a copy of the Program. Ancillary propagation of a covered work 422 | occurring solely as a consequence of using peer-to-peer transmission 423 | to receive a copy likewise does not require acceptance. However, 424 | nothing other than this License grants you permission to propagate or 425 | modify any covered work. These actions infringe copyright if you do 426 | not accept this License. Therefore, by modifying or propagating a 427 | covered work, you indicate your acceptance of this License to do so. 428 | 429 | #### 10. Automatic Licensing of Downstream Recipients. 430 | 431 | Each time you convey a covered work, the recipient automatically 432 | receives a license from the original licensors, to run, modify and 433 | propagate that work, subject to this License. You are not responsible 434 | for enforcing compliance by third parties with this License. 435 | 436 | An "entity transaction" is a transaction transferring control of an 437 | organization, or substantially all assets of one, or subdividing an 438 | organization, or merging organizations. If propagation of a covered 439 | work results from an entity transaction, each party to that 440 | transaction who receives a copy of the work also receives whatever 441 | licenses to the work the party's predecessor in interest had or could 442 | give under the previous paragraph, plus a right to possession of the 443 | Corresponding Source of the work from the predecessor in interest, if 444 | the predecessor has it or can get it with reasonable efforts. 445 | 446 | You may not impose any further restrictions on the exercise of the 447 | rights granted or affirmed under this License. For example, you may 448 | not impose a license fee, royalty, or other charge for exercise of 449 | rights granted under this License, and you may not initiate litigation 450 | (including a cross-claim or counterclaim in a lawsuit) alleging that 451 | any patent claim is infringed by making, using, selling, offering for 452 | sale, or importing the Program or any portion of it. 453 | 454 | #### 11. Patents. 455 | 456 | A "contributor" is a copyright holder who authorizes use under this 457 | License of the Program or a work on which the Program is based. The 458 | work thus licensed is called the contributor's "contributor version". 459 | 460 | A contributor's "essential patent claims" are all patent claims owned 461 | or controlled by the contributor, whether already acquired or 462 | hereafter acquired, that would be infringed by some manner, permitted 463 | by this License, of making, using, or selling its contributor version, 464 | but do not include claims that would be infringed only as a 465 | consequence of further modification of the contributor version. For 466 | purposes of this definition, "control" includes the right to grant 467 | patent sublicenses in a manner consistent with the requirements of 468 | this License. 469 | 470 | Each contributor grants you a non-exclusive, worldwide, royalty-free 471 | patent license under the contributor's essential patent claims, to 472 | make, use, sell, offer for sale, import and otherwise run, modify and 473 | propagate the contents of its contributor version. 474 | 475 | In the following three paragraphs, a "patent license" is any express 476 | agreement or commitment, however denominated, not to enforce a patent 477 | (such as an express permission to practice a patent or covenant not to 478 | sue for patent infringement). To "grant" such a patent license to a 479 | party means to make such an agreement or commitment not to enforce a 480 | patent against the party. 481 | 482 | If you convey a covered work, knowingly relying on a patent license, 483 | and the Corresponding Source of the work is not available for anyone 484 | to copy, free of charge and under the terms of this License, through a 485 | publicly available network server or other readily accessible means, 486 | then you must either (1) cause the Corresponding Source to be so 487 | available, or (2) arrange to deprive yourself of the benefit of the 488 | patent license for this particular work, or (3) arrange, in a manner 489 | consistent with the requirements of this License, to extend the patent 490 | license to downstream recipients. "Knowingly relying" means you have 491 | actual knowledge that, but for the patent license, your conveying the 492 | covered work in a country, or your recipient's use of the covered work 493 | in a country, would infringe one or more identifiable patents in that 494 | country that you have reason to believe are valid. 495 | 496 | If, pursuant to or in connection with a single transaction or 497 | arrangement, you convey, or propagate by procuring conveyance of, a 498 | covered work, and grant a patent license to some of the parties 499 | receiving the covered work authorizing them to use, propagate, modify 500 | or convey a specific copy of the covered work, then the patent license 501 | you grant is automatically extended to all recipients of the covered 502 | work and works based on it. 503 | 504 | A patent license is "discriminatory" if it does not include within the 505 | scope of its coverage, prohibits the exercise of, or is conditioned on 506 | the non-exercise of one or more of the rights that are specifically 507 | granted under this License. You may not convey a covered work if you 508 | are a party to an arrangement with a third party that is in the 509 | business of distributing software, under which you make payment to the 510 | third party based on the extent of your activity of conveying the 511 | work, and under which the third party grants, to any of the parties 512 | who would receive the covered work from you, a discriminatory patent 513 | license (a) in connection with copies of the covered work conveyed by 514 | you (or copies made from those copies), or (b) primarily for and in 515 | connection with specific products or compilations that contain the 516 | covered work, unless you entered into that arrangement, or that patent 517 | license was granted, prior to 28 March 2007. 518 | 519 | Nothing in this License shall be construed as excluding or limiting 520 | any implied license or other defenses to infringement that may 521 | otherwise be available to you under applicable patent law. 522 | 523 | #### 12. No Surrender of Others' Freedom. 524 | 525 | If conditions are imposed on you (whether by court order, agreement or 526 | otherwise) that contradict the conditions of this License, they do not 527 | excuse you from the conditions of this License. If you cannot convey a 528 | covered work so as to satisfy simultaneously your obligations under 529 | this License and any other pertinent obligations, then as a 530 | consequence you may not convey it at all. For example, if you agree to 531 | terms that obligate you to collect a royalty for further conveying 532 | from those to whom you convey the Program, the only way you could 533 | satisfy both those terms and this License would be to refrain entirely 534 | from conveying the Program. 535 | 536 | #### 13. Remote Network Interaction; Use with the GNU General Public License. 537 | 538 | Notwithstanding any other provision of this License, if you modify the 539 | Program, your modified version must prominently offer all users 540 | interacting with it remotely through a computer network (if your 541 | version supports such interaction) an opportunity to receive the 542 | Corresponding Source of your version by providing access to the 543 | Corresponding Source from a network server at no charge, through some 544 | standard or customary means of facilitating copying of software. This 545 | Corresponding Source shall include the Corresponding Source for any 546 | work covered by version 3 of the GNU General Public License that is 547 | incorporated pursuant to the following paragraph. 548 | 549 | Notwithstanding any other provision of this License, you have 550 | permission to link or combine any covered work with a work licensed 551 | under version 3 of the GNU General Public License into a single 552 | combined work, and to convey the resulting work. The terms of this 553 | License will continue to apply to the part which is the covered work, 554 | but the work with which it is combined will remain governed by version 555 | 3 of the GNU General Public License. 556 | 557 | #### 14. Revised Versions of this License. 558 | 559 | The Free Software Foundation may publish revised and/or new versions 560 | of the GNU Affero General Public License from time to time. Such new 561 | versions will be similar in spirit to the present version, but may 562 | differ in detail to address new problems or concerns. 563 | 564 | Each version is given a distinguishing version number. If the Program 565 | specifies that a certain numbered version of the GNU Affero General 566 | Public License "or any later version" applies to it, you have the 567 | option of following the terms and conditions either of that numbered 568 | version or of any later version published by the Free Software 569 | Foundation. If the Program does not specify a version number of the 570 | GNU Affero General Public License, you may choose any version ever 571 | published by the Free Software Foundation. 572 | 573 | If the Program specifies that a proxy can decide which future versions 574 | of the GNU Affero General Public License can be used, that proxy's 575 | public statement of acceptance of a version permanently authorizes you 576 | to choose that version for the Program. 577 | 578 | Later license versions may give you additional or different 579 | permissions. However, no additional obligations are imposed on any 580 | author or copyright holder as a result of your choosing to follow a 581 | later version. 582 | 583 | #### 15. Disclaimer of Warranty. 584 | 585 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 586 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 587 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT 588 | WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT 589 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 590 | A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND 591 | PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE 592 | DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR 593 | CORRECTION. 594 | 595 | #### 16. Limitation of Liability. 596 | 597 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 598 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR 599 | CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 600 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES 601 | ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT 602 | NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR 603 | LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM 604 | TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER 605 | PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 606 | 607 | #### 17. Interpretation of Sections 15 and 16. 608 | 609 | If the disclaimer of warranty and limitation of liability provided 610 | above cannot be given local legal effect according to their terms, 611 | reviewing courts shall apply local law that most closely approximates 612 | an absolute waiver of all civil liability in connection with the 613 | Program, unless a warranty or assumption of liability accompanies a 614 | copy of the Program in return for a fee. 615 | 616 | END OF TERMS AND CONDITIONS 617 | 618 | ### How to Apply These Terms to Your New Programs 619 | 620 | If you develop a new program, and you want it to be of the greatest 621 | possible use to the public, the best way to achieve this is to make it 622 | free software which everyone can redistribute and change under these 623 | terms. 624 | 625 | To do so, attach the following notices to the program. It is safest to 626 | attach them to the start of each source file to most effectively state 627 | the exclusion of warranty; and each file should have at least the 628 | "copyright" line and a pointer to where the full notice is found. 629 | 630 | 631 | Copyright (C) 632 | 633 | This program is free software: you can redistribute it and/or modify 634 | it under the terms of the GNU Affero General Public License as 635 | published by the Free Software Foundation, either version 3 of the 636 | License, or (at your option) any later version. 637 | 638 | This program is distributed in the hope that it will be useful, 639 | but WITHOUT ANY WARRANTY; without even the implied warranty of 640 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 641 | GNU Affero General Public License for more details. 642 | 643 | You should have received a copy of the GNU Affero General Public License 644 | along with this program. If not, see . 645 | 646 | Also add information on how to contact you by electronic and paper 647 | mail. 648 | 649 | If your software can interact with users remotely through a computer 650 | network, you should also make sure that it provides a way for users to 651 | get its source. For example, if your program is a web application, its 652 | interface could display a "Source" link that leads users to an archive 653 | of the code. There are many ways you could offer source, and different 654 | solutions will be better for different programs; see section 13 for 655 | the specific requirements. 656 | 657 | You should also get your employer (if you work as a programmer) or 658 | school, if any, to sign a "copyright disclaimer" for the program, if 659 | necessary. For more information on this, and how to apply and follow 660 | the GNU AGPL, see . 661 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | .PHONY: generate-self-signed 3 | generate-self-signed: 4 | openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout ./docker/openid-connect.test.key -out ./docker/openid-connect.test.crt -subj '/CN=openid-connect.test' 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel OpenID Connect Server 2 | 3 | ![](https://github.com/arietimmerman/laravel-openid-connect-server/workflows/CI/badge.svg) 4 | ![](https://img.shields.io/badge/license-AGPL--3.0-green) 5 | [![Latest Stable Version](https://poser.pugx.org/nl.idaas/laravel-openid-connect/v/stable)](https://packagist.org/packages/nl.idaas/laravel-openid-connect) 6 | [![Total Downloads](https://poser.pugx.org/nl.idaas/laravel-openid-connect/downloads)](https://packagist.org/packages/nl.idaas/laravel-openid-connect) 7 | 8 | This is an OpenID Connect Server written in PHP, built on top of [arietimmerman/openid-connect-server](https://github.com/arietimmerman/openid-connect-server) and [Laravel Passport](https://github.com/laravel/passport). 9 | 10 | This library is __work in progress__. 11 | 12 | ## Installation 13 | 14 | ~~~ 15 | composer require nl.idaas/laravel-openid-connect 16 | php artisan migrate 17 | php artisan passport:install --uuids 18 | php artisan vendor:publish --provider="Idaas\Passport\PassportServiceProvider" --force 19 | ~~~ 20 | 21 | ## Example 22 | 23 | ~~~ 24 | docker-compose build 25 | docker-compose up -d 26 | ~~~ 27 | 28 | Now find your `openid-configuration` at `http://localhost:18124/.well-known/openid-configuration`. 29 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nl.idaas/laravel-openid-connect", 3 | "description": "Drop-in replacement for Laravel Passport to add support for OpenID Connect", 4 | "type": "library", 5 | "authors": [ 6 | { 7 | "name": "Arie Timmerman", 8 | "email": "arietimmerman@a11n.nl" 9 | } 10 | ], 11 | "minimum-stability": "stable", 12 | "autoload": { 13 | "psr-4": { 14 | "Idaas\\Passport\\": "src/" 15 | } 16 | }, 17 | "config": { 18 | "preferred-install": { 19 | "laravel/passport": "source", 20 | "*": "dist" 21 | } 22 | }, 23 | "autoload-dev": { 24 | "psr-4": { 25 | "IdaasPassportTests\\": "tests/", 26 | "Laravel\\Passport\\Tests\\": "vendor/laravel/passport/tests/", 27 | "Workbench\\App\\": "workbench/app/", 28 | "Workbench\\Database\\Factories\\": "workbench/database/factories/" 29 | } 30 | }, 31 | "extra": { 32 | "laravel": { 33 | "providers": [ 34 | "Idaas\\Passport\\PassportServiceProvider" 35 | ] 36 | } 37 | }, 38 | "require": { 39 | "laravel/legacy-factories": "^1.3", 40 | "nl.idaas/openid-server": "^0.4.2", 41 | "illuminate/http": "^10.0|^11.0|^12.0", 42 | "illuminate/contracts": "^10.0|^11.0|^12.0", 43 | "illuminate/support": "^10.0|^11.0|^12.0", 44 | "illuminate/auth": "^10.0|^11.0|^12.0", 45 | "illuminate/database": "^10.0|^11.0|^12.0", 46 | "laravel/passport": "^10.0|^11.0|^12.0" 47 | }, 48 | "require-dev": { 49 | "mockery/mockery": "^1.0", 50 | "orchestra/testbench": "^6.0|^7.0|^8", 51 | "phpunit/phpunit": "^9.3" 52 | }, 53 | "license": "LGPL-3.0-only" 54 | } 55 | -------------------------------------------------------------------------------- /database/migrations/2018_05_11_203533_create_open_i_d_providers_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 18 | 19 | $table->integer('liftime_access_token'); 20 | $table->integer('liftime_refresh_token'); 21 | $table->integer('liftime_id_token'); 22 | 23 | $table->string('response_types_supported'); 24 | $table->string('acr_values_supported')->nullable(); 25 | 26 | $table->string('profile_url_template', 255)->nullable(); 27 | 28 | $table->string('init_url', 255)->nullable(); 29 | 30 | $table->timestamps(); 31 | }); 32 | } 33 | 34 | /** 35 | * Reverse the migrations. 36 | * 37 | * @return void 38 | */ 39 | public function down() 40 | { 41 | Schema::dropIfExists('open_i_d_providers'); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /database/migrations/2020_01_01_000001_create_oidc_clients_table.php: -------------------------------------------------------------------------------- 1 | uuid('client_id')->primary(); 19 | 20 | // TODO: not used?? 21 | $table->uuid('user_id')->index()->nullable(); 22 | $table->string('name'); 23 | 24 | //client_secret 25 | $table->string('secret', 100); 26 | 27 | $table->text('redirect_uris')->nullable(); 28 | 29 | $table->text('post_logout_redirect_uris')->nullable(); 30 | 31 | $table->boolean('personal_access_client'); 32 | $table->boolean('password_client'); 33 | $table->boolean('revoked'); 34 | $table->timestamps(); 35 | 36 | $table->string('response_types')->default('["token","code","id_token"]')->nullable(); 37 | 38 | $table->string('grant_types')->default('["authorization_code"]'); 39 | 40 | $table->string('code_challenge_methods_supported')->nullable(); 41 | 42 | $table->string('application_type')->default('web'); 43 | $table->string('public')->default('confidential'); 44 | $table->text('contacts')->nullable(); 45 | $table->string('logo_uri')->nullable(); 46 | $table->string('client_uri')->nullable(); 47 | $table->string('policy_uri')->nullable(); 48 | $table->string('tos_uri')->nullable(); 49 | 50 | $table->string('token_endpoint_auth_method')->default('client_secret_post'); 51 | 52 | $table->string('jwks_uri')->nullable(); 53 | $table->text('jwks')->nullable(); 54 | 55 | $table->string('default_max_age')->nullable(); 56 | $table->text('default_acr_values')->nullable(); 57 | 58 | $table->string('default_prompt')->nullable(); 59 | 60 | $table->boolean('default_prompt_allow_override')->default(true); 61 | $table->boolean('default_acr_values_allow_override')->default(true); 62 | 63 | $table->string('require_auth_time')->nullable(); 64 | 65 | $table->string('initiate_login_uri')->nullable(); 66 | 67 | $table->boolean('trusted')->default(false); 68 | }); 69 | } 70 | 71 | /** 72 | * Reverse the migrations. 73 | * 74 | * @return void 75 | */ 76 | public function down() 77 | { 78 | Schema::drop('oidc_clients'); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /database/migrations/2020_01_02_000001_create_oidc_personal_access_clients_table.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 18 | $table->uuid('client_id')->index(); 19 | $table->timestamps(); 20 | }); 21 | } 22 | 23 | /** 24 | * Reverse the migrations. 25 | * 26 | * @return void 27 | */ 28 | public function down() 29 | { 30 | Schema::dropIfExists('oidc_personal_access_clients'); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | 2 | version: '3' 3 | services: 4 | laravel-openid-connect: 5 | build: . 6 | ports: 7 | - "127.0.0.1:8000:8000" 8 | volumes: 9 | - .:/laravel-openid-connect 10 | environment: 11 | - APP_URL=http://laravel-openid-connect:8000 12 | networks: 13 | - default 14 | caddy: 15 | image: caddy/caddy 16 | ports: 17 | - "1443:1443" 18 | volumes: 19 | - ./Caddyfile:/etc/caddy/Caddyfile 20 | - ./docker:/docker 21 | networks: 22 | default: 23 | aliases: 24 | - openid-connect.test 25 | op-test: 26 | image: openidcertification/op_test 27 | entrypoint: bash -c "update-ca-certificates && ./run.sh" 28 | volumes: 29 | - ./docker:/usr/local/share/ca-certificates/docker 30 | ports: 31 | - "60000-60010:60000-60010" 32 | networks: 33 | - default 34 | 35 | networks: 36 | default: 37 | -------------------------------------------------------------------------------- /examples/App/Providers/AuthServiceProvider.php: -------------------------------------------------------------------------------- 1 | 'App\Policies\ModelPolicy', 18 | ]; 19 | 20 | /** 21 | * Register any authentication / authorization services. 22 | * 23 | * @return void 24 | */ 25 | public function boot() 26 | { 27 | $this->registerPolicies(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /examples/App/User.php: -------------------------------------------------------------------------------- 1 | 'datetime', 38 | ]; 39 | 40 | } 41 | -------------------------------------------------------------------------------- /examples/config/auth.php: -------------------------------------------------------------------------------- 1 | [ 6 | 'guard' => 'web', 7 | 'passwords' => 'users', 8 | ], 9 | 10 | 'guards' => [ 11 | 'web' => [ 12 | 'driver' => 'session', 13 | 'provider' => 'users', 14 | ], 15 | 16 | 'api' => [ 17 | 'driver' => 'passport', 18 | 'provider' => 'users', 19 | 'hash' => false, 20 | ], 21 | ], 22 | 23 | 'providers' => [ 24 | 'users' => [ 25 | 'driver' => 'eloquent', 26 | 'model' => App\User::class, 27 | ], 28 | 29 | // 'users' => [ 30 | // 'driver' => 'database', 31 | // 'table' => 'users', 32 | // ], 33 | ], 34 | 35 | 'passwords' => [ 36 | 'users' => [ 37 | 'provider' => 'users', 38 | 'table' => 'password_resets', 39 | 'expire' => 60, 40 | 'throttle' => 60, 41 | ], 42 | ], 43 | 44 | 'password_timeout' => 10800, 45 | 46 | ]; 47 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ./tests 6 | 7 | 8 | 9 | 10 | ./src 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/Bridge/AccessToken.php: -------------------------------------------------------------------------------- 1 | claims = $claims; 19 | } 20 | 21 | /** 22 | * Return an array of scopes associated with the token. 23 | * 24 | * @return ClaimEntityInterface[] 25 | */ 26 | public function getClaims() 27 | { 28 | return $this->claims; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Bridge/AccessTokenRepository.php: -------------------------------------------------------------------------------- 1 | tokenRepository->find($token->getIdentifier()); 18 | $token->claims = $claims; 19 | $token->save(); 20 | } 21 | 22 | public function getAccessToken($id) 23 | { 24 | $token = $this->tokenRepository->find($id); 25 | 26 | $claims = ClaimEntity::fromJsonArray($token->claims ?? []); 27 | 28 | return new AccessToken( 29 | $token->user_id, 30 | collect($token->scopes)->map(function ($scope) { 31 | return new Scope($scope); 32 | })->toArray(), 33 | new Client('not used', 'not used', 'not used', false), 34 | $claims 35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Bridge/ClaimEntity.php: -------------------------------------------------------------------------------- 1 | identifier = $identifier; 16 | $this->type = $type; 17 | $this->essential = $essential; 18 | } 19 | 20 | public static function fromJson(array $json) 21 | { 22 | return new ClaimEntity($json['identifier'], $json['type'], $json['essential']); 23 | } 24 | 25 | public static function fromJsonArray(array $json) 26 | { 27 | return collect($json)->map(function ($value) { 28 | return self::fromJson($value); 29 | })->toArray(); 30 | } 31 | 32 | public function getIdentifier() 33 | { 34 | return $this->identifier; 35 | } 36 | 37 | public function getType() 38 | { 39 | return $this->type; 40 | } 41 | 42 | public function getEssential() 43 | { 44 | return $this->essential; 45 | } 46 | 47 | public function jsonSerialize(): array 48 | { 49 | return [ 50 | 'identifier' => $this->getIdentifier(), 51 | 'type' => $this->getType(), 52 | 'essential' => $this->getEssential() 53 | ]; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Bridge/ClaimRepository.php: -------------------------------------------------------------------------------- 1 | ['name', 'family_name', 'given_name', 'middle_name', 'nickname', 'preferred_username', 'profile', 'picture', 'website', 'gender', 'birthdate', 'zoneinfo', 'locale', 'updated_at'], 13 | 'email' => ['email', 'email_verified'], 14 | 'address' => ['address'], 15 | 'phone' => ['phone_number', 'phone_number_verified'], 16 | ]; 17 | 18 | public function getScopeClaims() 19 | { 20 | return self::$scopeClaims; 21 | } 22 | 23 | public function getClaimEntityByIdentifier($identifier, $type, $essential) 24 | { 25 | return new ClaimEntity($identifier, $type, $essential); 26 | } 27 | 28 | public function getClaimsByScope(ScopeEntityInterface $scope): iterable 29 | { 30 | $scope = $scope->getIdentifier(); 31 | 32 | $result = []; 33 | 34 | $map = $this->getScopeClaims(); 35 | 36 | if (isset($map[$scope])) { 37 | foreach ($map[$scope] as $claim) { 38 | $result[] = new ClaimEntity( 39 | $claim, 40 | ClaimEntity::TYPE_USERINFO, 41 | false 42 | ); 43 | } 44 | } 45 | 46 | return $result; 47 | } 48 | 49 | public function claimsRequestToEntities(array $json = null) 50 | { 51 | $result = []; 52 | 53 | foreach ([ClaimEntity::TYPE_ID_TOKEN, ClaimEntity::TYPE_USERINFO] as $type) { 54 | if ($json != null && isset($json[$type])) { 55 | foreach ($json[$type] as $claim => $properties) { 56 | $result[] = new ClaimEntity( 57 | $claim, 58 | $type, 59 | isset($properties) && isset($properties['essential']) ? $properties['essential'] : false 60 | ); 61 | } 62 | } 63 | } 64 | 65 | return $result; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Bridge/ClientRepository.php: -------------------------------------------------------------------------------- 1 | grant_types ?? []) as $v) { 13 | $result[] = $v; 14 | $result[] = $v . '_oidc'; 15 | } 16 | 17 | return $record->application_type != null && in_array($grantType, $result); 18 | } 19 | 20 | public function all() 21 | { 22 | return $this->clients->all(); 23 | } 24 | 25 | public function findForManagement($clientId) 26 | { 27 | return $this->clients->findForManagement($clientId); 28 | } 29 | 30 | public function getRepository() 31 | { 32 | return $this->clients; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Bridge/UserRepository.php: -------------------------------------------------------------------------------- 1 | $user->getIdentifier() 24 | ]; 25 | } 26 | 27 | public function getUserInfoAttributes(UserEntityInterface $user, $claims, $scopes) 28 | { 29 | return $this->getAttributes($user, $claims, $scopes); 30 | } 31 | 32 | public function getUserByIdentifier($identifier) : ?UserEntityInterface 33 | { 34 | $provider = config('auth.guards.api.provider'); 35 | 36 | if (is_null($model = config('auth.providers.' . $provider . '.model'))) { 37 | throw new RuntimeException('Unable to determine authentication model from configuration.'); 38 | } 39 | 40 | if (method_exists($model, 'findForPassport')) { 41 | $user = (new $model)->findForPassport($identifier); 42 | } else { 43 | $user = (new $model)->where('email', $identifier)->first(); 44 | } 45 | 46 | if (!$user) { 47 | return null; 48 | } 49 | 50 | return new User($user->getAuthIdentifier()); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/ClientController.php: -------------------------------------------------------------------------------- 1 | 'in:web,native', 34 | 'public' => 'in:public,confidential', 35 | 'redirect_uris' => 'nullable|array', 36 | 'redirect_uris.*' => ['required','url'], 37 | 38 | 'post_logout_redirect_uris' => 'nullable|array', 39 | 'post_logout_redirect_uris.*' => ['required','url'], 40 | 41 | 'response_types' => 'nullable|array', 42 | 'response_types.*' => 'in:code,id_token,token|distinct', 43 | 'grant_types' => 'nullable|array', 44 | 'grant_types.*' => 'in:authorization_code,implicit,refresh_token,client_credentials', 45 | 46 | 'code_challenge_methods_supported' => 'nullable|array|in:plain,S256', 47 | 48 | 'contacts' => 'nullable|array', 49 | 'contacts.*' => 'email|distinct', 50 | 51 | // strictly not required according to OIDC specs 52 | 'client_name' => 'required|unique:oidc_clients,name|max:255', 53 | 54 | 'logo_uri' => 'nullable', 55 | 'client_uri' => 'nullable|url', 56 | 'policy_uri' => 'nullable|url', 57 | 'tos_uri' => 'nullable|url', 58 | 59 | 'token_endpoint_auth_method' => 'nullable|in:client_secret_post,none', 60 | 61 | 'default_max_age' => 'nullable|integer|min:0', 62 | 63 | 'default_prompt' => 'nullable|in:login,none,consent', 64 | 'default_prompt_allow_override' => 'nullable|boolean', 65 | 'default_acr_values_allow_override' => 'nullable|boolean', 66 | 67 | 'require_auth_time' => 'nullable|integer|min:0', 68 | 69 | // 'default_acr_values' => 'nullable|array', 70 | // 'default_acr_values.*' => 'nullable|array', 71 | 'initiate_login_uri' => 'nullable|url', 72 | 73 | 'trusted' => 'nullable|boolean', 74 | 75 | 'user_interface' => 'nullable|exists:u_i_servers,id', 76 | 77 | ]; 78 | 79 | protected $messages = [ 80 | 'redirect_uris.*' => 'One or more values does not represent a valid url', 81 | 'post_logout_redirect_uris.*' => 'One or more values does not represent a valid url' 82 | ]; 83 | 84 | /** 85 | * Create a client controller instance. 86 | * 87 | * @param \Idaas\Passport\ClientRepository $clients 88 | * @param \Illuminate\Contracts\Validation\Factory $validation 89 | * @return void 90 | */ 91 | public function __construct( 92 | ClientRepository $clients, 93 | ValidationFactory $validation 94 | ) { 95 | $this->clients = $clients; 96 | $this->validation = $validation; 97 | } 98 | 99 | /** 100 | * Get all of the clients for the authenticated user. 101 | * 102 | * @param \Illuminate\Http\Request $request 103 | * @return \Illuminate\Http\Response 104 | */ 105 | public function forUser(Request $request) 106 | { 107 | return $this->clients->all(); 108 | } 109 | 110 | /** 111 | * Store a new client. 112 | * 113 | * @param \Illuminate\Http\Request $request 114 | * @return \Illuminate\Http\Response 115 | */ 116 | public function store(Request $request) 117 | { 118 | 119 | $data = $this->validate($request, $this->validations, $this->messages); 120 | $client = $this->clients->getRepository()->create( 121 | $request->user()->getKey(), 122 | $request->client_name, 123 | $request->redirect_uris 124 | )->makeVisible('secret'); 125 | 126 | $client->forceFill($data); 127 | 128 | $client->save(); 129 | 130 | return $client; 131 | } 132 | 133 | 134 | public function get($clientId) 135 | { 136 | //TODO: Add some form of authorization 137 | return $this->clients->findForManagement($clientId); 138 | } 139 | 140 | /** 141 | * Update the given client. 142 | * 143 | * @param \Illuminate\Http\Request $request 144 | * @param string $clientId 145 | * @return \Illuminate\Http\Response|\ArieTimmerman\Passport\Client 146 | */ 147 | public function update(Request $request, $clientId) 148 | { 149 | $client = $this->clients->findForManagement($clientId); 150 | 151 | if (! $client) { 152 | return new Response('', 404); 153 | } 154 | 155 | $validations = $this->validations; 156 | 157 | if ($request->input('client_name') == $client->client_name) { 158 | unset($validations['client_name']); 159 | } 160 | 161 | $data = $this->validate($request, $validations, $this->messages); 162 | 163 | $client->forceFill($data)->save(); 164 | 165 | return $client; 166 | } 167 | 168 | /** 169 | * Delete the given client. 170 | * 171 | * @param Request $request 172 | * @param string $clientId 173 | * @return Response 174 | */ 175 | public function destroy(Request $request, $clientId) 176 | { 177 | $client = $this->clients->findForManagement($clientId); 178 | 179 | if (! $client) { 180 | return new Response('', 404); 181 | } 182 | 183 | $this->clients->getRepository()->delete( 184 | $client 185 | ); 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /src/ClientRepository.php: -------------------------------------------------------------------------------- 1 | false])->orderBy('name', 'asc')->get(); 14 | } 15 | 16 | public function findForManagement($id) 17 | { 18 | $client = Passport::client(); 19 | return $client::find($id); 20 | } 21 | 22 | public function create($userId, $name, $redirect, $provider = null, $personalAccess = false, $password = false, $confidential = true) 23 | { 24 | $client = Passport::client()->forceFill([ 25 | 'user_id' => $userId, 26 | 'client_name' => $name, 27 | 'secret' => Str::random(40), 28 | 'redirect_uris' => (is_array($redirect)) ? $redirect : [$redirect], 29 | 'personal_access_client' => $personalAccess, 30 | 'password_client' => $password, 31 | 'grant_types' => ($password) ? ["authorization_code", "password"] : ["authorization_code"], 32 | 'revoked' => false, 33 | ]); 34 | 35 | $client->save(); 36 | 37 | return $client; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Guards/TokenGuard.php: -------------------------------------------------------------------------------- 1 | request->input('access_token')) != null) { 28 | $this->request->headers->set('Authorization', 'Bearer ' . $access_token); 29 | $result = $this->authenticateViaBearerToken($this->request); 30 | } 31 | 32 | return $result; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Http/Controllers/AccessTokenController.php: -------------------------------------------------------------------------------- 1 | getErrorType() == 'invalid_request' && $e->getHint() == 'Authorization code has been revoked') { 23 | 24 | // TOOD: Not needed as soon as https://github.com/thephpleague/oauth2-server/pull/1082 is used. 25 | $e = LeagueException::invalidGrant($e->getHint()); 26 | 27 | // TODO: This is an ugly workaround to revoke an earlier issued access token if an authorization code 28 | // is used twice 29 | $encryptedAuthCode = $_POST['code']; 30 | $this->setEncryptionKey(app('encrypter')->getKey()); 31 | $authCodePayload = \json_decode($this->decrypt($encryptedAuthCode)); 32 | Passport::token()->where('user_id', $authCodePayload->user_id)->update(['revoked' => true]); 33 | } 34 | 35 | throw new OAuthServerException( 36 | $e, 37 | $this->convertResponse($e->generateHttpResponse(new Psr7Response)) 38 | ); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Http/Controllers/AuthorizationController.php: -------------------------------------------------------------------------------- 1 | parseScopes($authRequest); 34 | 35 | $token = $tokens->findValidToken( 36 | $user, 37 | $client 38 | ); 39 | 40 | return ($token && $token->scopes === collect($scopes)->pluck('id')->all()); 41 | } 42 | 43 | public function returnError(AuthorizationRequest $authorizationRequest, Request $request) 44 | { 45 | $clientUris = Arr::wrap($authorizationRequest->getClient()->getRedirectUri()); 46 | 47 | if (!in_array($uri = $authorizationRequest->getRedirectUri(), $clientUris)) { 48 | $uri = Arr::first($clientUris); 49 | } 50 | 51 | if ($authorizationRequest instanceof AuthenticationRequest && $authorizationRequest->getResponseMode() == 'web_message') { 52 | return (new WebMessageResponse())->setData([ 53 | 'redirect_uri' => $uri, 54 | 'error' => 'access_denied', 55 | 'state' => $authorizationRequest->getState(), 56 | ])->generateHttpResponse(new Psr7Response()); 57 | } else { 58 | $separator = $authorizationRequest->getGrantTypeId() === 'implicit' ? '#' : '?'; 59 | $uri = $uri . $separator . 'error=access_denied&state=' . $authorizationRequest->getState(); 60 | 61 | return $this->withErrorHandling(function () use ($uri) { 62 | throw OAuthServerException::accessDenied(null, $uri); 63 | }); 64 | } 65 | } 66 | 67 | /** 68 | * In contrast with Laravel Passport, this authorize method can be invoked when the user has not been authenticated 69 | * This is because the OpenID Connect determines how to user should be authenticated 70 | */ 71 | public function authorize( 72 | ServerRequestInterface $psrRequest, 73 | Request $request, 74 | LaravelClientRepository $clients, 75 | TokenRepository $tokens 76 | ) { 77 | return $this->withErrorHandling(function () use ($psrRequest, $request, $clients, $tokens) { 78 | 79 | $authorizationRequest = $this->server->validateAuthorizationRequest($psrRequest); 80 | $authenticateResponse = $this->doAuthenticate($psrRequest, $authorizationRequest); 81 | 82 | if ($authenticateResponse == null) { 83 | $authenticateResponse = $this->continueAuthorize($authorizationRequest, $request, $clients, $tokens); 84 | } 85 | return $authenticateResponse; 86 | }); 87 | } 88 | 89 | public function continueAuthorize( 90 | AuthorizationRequest $authRequest = null, 91 | Request $request, 92 | ClientRepository $clients, 93 | TokenRepository $tokens 94 | ) { 95 | // If $authRequest is not provided as a parameter, load it from a session 96 | if ($authRequest == null) { 97 | $authRequest = $request->session()->get('authRequest'); 98 | } 99 | 100 | if ($authRequest == null) { 101 | throw OAuthServerException::invalidRequest('unknown', 'No authorization request found. Seems like a cookie problem.'); 102 | } 103 | 104 | $user = $request->user(); 105 | $client = $clients->find($authRequest->getClient()->getIdentifier()); 106 | 107 | if ($client->skipsAuthorization() || $this->isApproved($authRequest, $user, $client, $tokens)) { 108 | return $this->approveRequest($authRequest, $user); 109 | } else { 110 | return $this->returnError($authRequest, $request); 111 | } 112 | } 113 | 114 | public function doAuthenticate(ServerRequestInterface $psrRequest, $authorizationRequest) 115 | { 116 | return resolve(PassportConfig::class) 117 | ->doAuthenticationResponse( 118 | AuthenticationRequest::fromAuthorizationRequest($authorizationRequest) 119 | ); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/IntrospectionController.php: -------------------------------------------------------------------------------- 1 | jwt = $jwt; 31 | } 32 | 33 | 34 | /** 35 | * Authorize a client to access the user's account. 36 | * 37 | * @param ServerRequestInterface $request 38 | * 39 | * @return JsonResponse|ResponseInterface 40 | */ 41 | public function introspect(ServerRequestInterface $request, BearerTokenValidator $validator) 42 | { 43 | if (array_get($request->getParsedBody(), 'token_type_hint', 'access_token') !== 'access_token') { 44 | // unsupported introspection 45 | return $this->notActiveResponse(); 46 | } 47 | 48 | $accessToken = array_get($request->getParsedBody(), 'token'); 49 | if ($accessToken === null) { 50 | return $this->notActiveResponse(); 51 | } 52 | 53 | try { 54 | $token = $this->jwt->parse($accessToken); 55 | 56 | try { 57 | $validator->ensureValidity($token); 58 | } catch (OAuthServerException $e) { 59 | return $this->notActiveResponse(); 60 | } 61 | 62 | return $this->jsonResponse([ 63 | 'active' => true, 64 | 'scope' => trim(implode(' ', (array)$token->getClaim('scopes', []))), 65 | 'client_id' => $token->getClaim('aud'), 66 | 'token_type' => 'access_token', 67 | 'exp' => intval($token->getClaim('exp')), 68 | 'iat' => intval($token->getClaim('iat')), 69 | 'nbf' => intval($token->getClaim('nbf')), 70 | 'sub' => $token->getClaim('sub'), 71 | 'aud' => $token->getClaim('aud'), 72 | 'jti' => $token->getClaim('jti'), 73 | ]); 74 | } catch (OAuthServerException $oAuthServerException) { 75 | return $oAuthServerException->generateHttpResponse(new Psr7Response); 76 | } catch (\Exception $exception) { 77 | return $this->exceptionResponse($exception); 78 | } 79 | } 80 | 81 | /** 82 | * returns inactive token message 83 | * 84 | * @return \Illuminate\Http\JsonResponse 85 | */ 86 | private function notActiveResponse() : JsonResponse 87 | { 88 | return $this->jsonResponse(['active' => false]); 89 | } 90 | 91 | /** 92 | * @param array|mixed $data 93 | * @param int $status 94 | * 95 | * @return \Illuminate\Http\JsonResponse 96 | */ 97 | private function jsonResponse($data, $status = 200) : JsonResponse 98 | { 99 | return new JsonResponse($data, $status); 100 | } 101 | 102 | /** 103 | * returns an error 104 | * 105 | * @param \Exception $exception 106 | * @param int $status 107 | * 108 | * @return \Illuminate\Http\JsonResponse 109 | */ 110 | private function exceptionResponse(\Exception $exception, $status = 500) : JsonResponse 111 | { 112 | return $this->notActiveResponse(); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/KeyRepository.php: -------------------------------------------------------------------------------- 1 | getPublicKey()]; 35 | } 36 | 37 | public function getPrivateKeyByKid($kid): CryptKey 38 | { 39 | return $this->getPrivateKey(); 40 | } 41 | 42 | public static function generateNew() 43 | { 44 | $dn = array( 45 | "countryName" => "NL", 46 | "stateOrProvinceName" => "Noord-Holland", 47 | "localityName" => "Hilversum", 48 | "organizationName" => "a11n", 49 | "organizationalUnitName" => "Developer", 50 | "commonName" => "a11n", 51 | "emailAddress" => "arietimmerman@a11n.nl" 52 | ); 53 | 54 | // Generate a new private (and public) key pair 55 | $privkey = openssl_pkey_new(array( 56 | "private_key_bits" => 2048, 57 | "private_key_type" => OPENSSL_KEYTYPE_RSA, 58 | )); 59 | 60 | // Generate a certificate signing request 61 | $csr = openssl_csr_new($dn, $privkey, array('digest_alg' => 'sha256')); 62 | 63 | // Generate a self-signed cert, valid for 365 days 64 | $x509 = openssl_csr_sign($csr, null, $privkey, 365, array('digest_alg' => 'sha256')); 65 | 66 | openssl_x509_export($x509, $certout); 67 | openssl_pkey_export($privkey, $pkeyout); 68 | 69 | $publicKey = openssl_pkey_get_details(openssl_pkey_get_public($x509)); 70 | 71 | return ['x509' => $certout, 'public_key' => $publicKey['key'], 'private_key' => $pkeyout]; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Middleware/AllowClientCORS.php: -------------------------------------------------------------------------------- 1 | server = $server; 35 | $this->clientRepository = $clientRepository; 36 | } 37 | 38 | public function isCORSAllowed(Request $request) 39 | { 40 | if ($request->method() == 'OPTIONS') { 41 | return true; 42 | } 43 | 44 | $origin = $request->headers->get('origin'); 45 | 46 | /* @var $token \Laravel\Passport\Token */ 47 | $token = $request->user()->token(); 48 | 49 | return resolve(TokenCache::class)->rememberOriginAllowed($origin, function () use ($request, $origin, $token) { 50 | $allowed = false; 51 | 52 | $client = $this->clientRepository->find($token->client_id); 53 | 54 | foreach ($client->redirect_uris as $redirectUri) { 55 | $parse = parse_url($redirectUri); 56 | 57 | $parse['port'] = $parse['port'] ?? 80; 58 | 59 | if ( 60 | $origin == null || 61 | $origin == ($parse['scheme'] . "://" . $parse['host'] . ':' . $parse['port']) || ($parse['port'] == 80 || $parse['port'] == 443) && $origin == ($parse['scheme'] . "://" . $parse['host']) 62 | ) { 63 | $allowed = true; 64 | break; 65 | } 66 | } 67 | 68 | return $allowed; 69 | }); 70 | } 71 | 72 | /** 73 | * Handle an incoming request. 74 | * 75 | * @param \Illuminate\Http\Request $request 76 | * @param \Closure $next 77 | * @param mixed ...$scopes 78 | * @return mixed 79 | * @throws \Illuminate\Auth\AuthenticationException 80 | */ 81 | public function handle($request, Closure $next) 82 | { 83 | if ($request->headers->get('origin') == null) { 84 | return $next($request); 85 | } elseif ($this->isCORSAllowed($request)) { 86 | $response = $next($request); 87 | 88 | 89 | $response->headers->set('Access-Control-Allow-Methods', $this->methods); 90 | $response->headers->set('Access-Control-Allow-Headers', $this->headers); 91 | $response->headers->set('Access-Control-Allow-Credentials', 'true'); 92 | $response->headers->set('Access-Control-Allow-Origin', $request->headers->get('origin')); 93 | $response->headers->set('Vary', 'Origin'); 94 | $response->headers->set('Access-Control-Max-Age', $this->maxAge); 95 | 96 | return $response; 97 | } else { 98 | return response(null, 403); 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/Model/Client.php: -------------------------------------------------------------------------------- 1 | 'array', 19 | 'grant_types' => 'array', 20 | 'response_types' => 'array', 21 | 'redirect_uris' => 'array', 22 | 'post_logout_redirect_uris' => 'array', 23 | 'code_challenge_methods_supported' => 'array', 24 | 'trusted' => 'boolean', 25 | 'default_prompt_allow_override' => 'boolean', 26 | 'default_acr_values_allow_override' => 'boolean', 27 | ]; 28 | 29 | protected $hidden = [ 30 | // 'secret', 31 | 'updated_at', 32 | 'created_at', 33 | 'user_id', 34 | 'revoked', 35 | 'personal_access_client', 36 | 'password_client', 37 | 'name' 38 | ]; 39 | 40 | protected $appends = ['client_name']; 41 | 42 | public static function boot() 43 | { 44 | parent::boot(); 45 | 46 | static::creating(function ($model) { 47 | $model->{$model->getKeyName()} = (string) Str::orderedUuid(); 48 | }); 49 | } 50 | 51 | public function getIdAttribute() 52 | { 53 | return $this->client_id; 54 | } 55 | 56 | // Ensure compatability with the default OAuth client 57 | public function getClientNameAttribute() 58 | { 59 | return $this->name; 60 | } 61 | 62 | public function setClientNameAttribute($value) 63 | { 64 | $this->attributes['name'] = $value; 65 | } 66 | 67 | 68 | // Ensure compatability with the defaukt OAuth client 69 | public function getRedirectAttribute() 70 | { 71 | return implode(',', $this->redirect_uris); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Model/PersonalAccessClient.php: -------------------------------------------------------------------------------- 1 | client_id = (string) Str::orderedUuid(); 19 | }); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Model/Provider.php: -------------------------------------------------------------------------------- 1 | wellKnown = array( 12 | 'response_types_supported' => 13 | [ 14 | 'code', 15 | 'token', 16 | 'id_token', 17 | 'code token', 18 | 'id_token token', 19 | ], 20 | 'acr_values_supported' => 21 | [ 22 | 'urn:mace:incommon:iap:gold', 23 | 'urn:mace:incommon:iap:silver', 24 | 'urn:mace:incommon:iap:bronze', 25 | ], 26 | 'scopes_supported' => 27 | [ 28 | 'openid', 29 | 'online_access', 30 | 'profile', 31 | 'email', 32 | 'address', 33 | 'phone', 34 | 'profile', 35 | 'email', 36 | 'address', 37 | 'phone', 38 | 'roles' 39 | ], 40 | 'authorization_endpoint' => route('oauth.authorize', []), 41 | 'token_endpoint' => route('oauth.token', []), 42 | 'userinfo_endpoint' => route('oidc.userinfo', []), 43 | 'jwks_uri' => route('oidc.jwks', []), 44 | 'issuer' => url('/'), 45 | 'claims_supported' => 46 | [ 47 | 'sub', 48 | 'iss', 49 | 'roles', 50 | 'acr', 51 | 'picture', 52 | 'profile', 53 | ], 54 | 'end_session_endpoint' => route('oidc.logout', []), 55 | 'code_challenge_methods_supported' => 56 | [ 57 | 'S256', 58 | ], 59 | 'introspection_endpoint' => route('oauth.introspect'), 60 | 'introspection_endpoint_auth_methods_supported' => 61 | [ 62 | 'client_secret_jwt', 63 | ], 64 | 'token_endpoint_auth_methods_supported' => 65 | [ 66 | 'none', 67 | 'client_secret_post', 68 | 'client_secret_basic', 69 | ], 70 | 'revocation_endpoint' => route('oauth.revoke'), 71 | 'service_documentation' => url('/'), 72 | 'ui_locales_supported' => 73 | [ 74 | 'en-GB', 75 | 'nl-NL', 76 | ], 77 | ); 78 | } 79 | 80 | public function toJson($options = 0) 81 | { 82 | return json_encode($this->wellKnown, $options); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Model/ProviderInterface.php: -------------------------------------------------------------------------------- 1 | Laravel Passport's routes are now registered in service provider and uses web.php file. 16 | *
To define routes this way we have to use Passport::ignoreRoutes() method and copy out the routes from 17 | * Passport's web.php file. See: https://github.com/laravel/passport/pull/1464 18 | * @param callable|null $callback 19 | * @param array $options 20 | * @return void 21 | */ 22 | public static function routes($callback = null, array $options = []) 23 | { 24 | $registerWellKnown = $callback === null; 25 | $callback = $callback ?: function (RouteRegistrar $router) { 26 | $router->all(); 27 | }; 28 | 29 | $defaultOptions = [ 30 | 'prefix' => 'oauth', 31 | 'namespace' => '\Laravel\Passport\Http\Controllers', 32 | ]; 33 | 34 | $options = array_merge($defaultOptions, $options); 35 | 36 | Route::group($options, function ($router) use ($callback) { 37 | $callback(new RouteRegistrar($router)); 38 | }); 39 | 40 | // The wellKnown endpoints must be registered without a prefix 41 | if ($registerWellKnown) { 42 | Route::group([ 43 | 'namespace' => '\Laravel\Passport\Http\Controllers' 44 | ], function ($router) use ($callback) { 45 | $router = new RouteRegistrar($router); 46 | $router->forWellKnown(); 47 | }); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/PassportConfig.php: -------------------------------------------------------------------------------- 1 | getClientModel()); 51 | Passport::usePersonalAccessClientModel($this->getPersonalAccessClientModel()); 52 | // Passport::useTokenModel() 53 | 54 | parent::boot(); 55 | 56 | $this->app->bindIf(ClaimRepositoryInterface::class, ClaimRepository::class); 57 | $this->app->bindIf(UserRepositoryInterface::class, UserRepository::class); 58 | 59 | $this->app->singleton(AccessTokenRepositoryInterface::class, function ($app) { 60 | return $this->app->make(AccessTokenRepository::class); 61 | }); 62 | $this->app->singleton(BridgeAccessTokenRepository::class, function ($app) { 63 | return $app->make(AccessTokenRepositoryInterface::class); 64 | }); 65 | 66 | $this->publishes( 67 | [ 68 | __DIR__ . '/../examples/App/User.php' => app_path('User.php'), 69 | __DIR__ . '/../examples/App/Providers/AuthServiceProvider.php' => 70 | app_path('Providers/AuthServiceProvider.php'), 71 | __DIR__ . '/../examples/config/auth.php' => 72 | config_path('auth.php'), 73 | ] 74 | ); 75 | } 76 | 77 | protected function makeCryptKey($type) 78 | { 79 | if ($type == 'private') { 80 | return resolve(KeyRepository::class)->getPrivateKey(); 81 | } else { 82 | return resolve(KeyRepository::class)->getPublicKey(); 83 | } 84 | } 85 | 86 | protected function registerResourceServer() 87 | { 88 | $this->app->singleton(ResourceServer::class, function () { 89 | // TODO: consider using AdvancedResourceServer 90 | return new ResourceServer( 91 | $this->app->make(Bridge\AccessTokenRepository::class), 92 | $this->makeCryptKey('public') 93 | ); 94 | }); 95 | } 96 | 97 | public function makeAuthorizationServer() 98 | { 99 | $server = new AuthorizationServer( 100 | $this->app->make(Bridge\ClientRepository::class), 101 | $this->app->make(Bridge\AccessTokenRepository::class), 102 | $this->app->make(ScopeRepository::class), 103 | resolve(KeyRepository::class)->getPrivateKey(), 104 | app('encrypter')->getKey(), 105 | new BearerTokenResponse 106 | ); 107 | 108 | $authCodeGrant = new AuthCodeGrant( 109 | $this->app->make(AuthCodeRepository::class), 110 | $this->app->make(RefreshTokenRepository::class), 111 | $this->app->make(ClaimRepositoryInterface::class), 112 | $this->app->make(Session::class), 113 | new DateInterval('PT10M'), 114 | new DateInterval('PT10M') 115 | ); 116 | $authCodeGrant->setIssuer(url('/')); 117 | 118 | $server->enableGrantType( 119 | $authCodeGrant 120 | ); 121 | 122 | $server->enableGrantType( 123 | new ImplicitGrant( 124 | $this->app->make(UserRepositoryInterface::class), 125 | $this->app->make(ClaimRepositoryInterface::class), 126 | $this->app->make(Session::class), 127 | new DateInterval('PT10M'), 128 | new DateInterval('PT10M') 129 | ) 130 | ); 131 | 132 | return $server; 133 | } 134 | 135 | /** 136 | * Register the client repository. 137 | * 138 | * @return void 139 | */ 140 | protected function registerClientRepository() 141 | { 142 | $this->app->singleton(PassportClientRepository::class, function ($container) { 143 | $config = $container->make('config')->get('passport.personal_access_client'); 144 | 145 | return new ClientRepository($config['id'] ?? null, $config['secret'] ?? null); 146 | }); 147 | } 148 | 149 | protected function buildAuthCodeGrant() 150 | { 151 | $grant = parent::buildAuthCodeGrant(); 152 | return $grant; 153 | } 154 | 155 | protected function makeGuard(array $config) 156 | { 157 | return new RequestGuard(function ($request) use ($config) { 158 | return (new TokenGuard( 159 | $this->app->make(ResourceServer::class), 160 | new PassportUserProvider(Auth::createUserProvider($config['provider']), 'users'), 161 | $this->app->make(TokenRepository::class), 162 | $this->app->make(ClientRepository::class), 163 | $this->app->make('encrypter'), 164 | $this->app['request'], 165 | ))->user(); 166 | }, $this->app['request']); 167 | } 168 | 169 | protected function registerMigrations() 170 | { 171 | parent::registerMigrations(); 172 | 173 | if (Passport::$runsMigrations) { 174 | $this->loadMigrationsFrom(__DIR__ . '/../database/migrations'); 175 | } 176 | } 177 | 178 | protected function registerRoutes() 179 | { 180 | parent::registerRoutes(); 181 | 182 | if (Passport::$registersRoutes) { 183 | Route::group([ 184 | 'namespace' => 'Laravel\Passport\Http\Controllers', 185 | ], function () { 186 | $this->loadRoutesFrom(__DIR__ . '/../src/routes/web.php'); 187 | }); 188 | } 189 | } 190 | 191 | public function register() 192 | { 193 | parent::register(); 194 | $this->app->when(AuthorizationController::class) 195 | ->needs(StatefulGuard::class) 196 | ->give(fn() => Auth::guard(config('passport.guard', null))); 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /src/ProviderController.php: -------------------------------------------------------------------------------- 1 | toWebSafe(base64_encode($input)); 18 | } 19 | 20 | public function index(ProviderRepository $providerRepository) 21 | { 22 | return $providerRepository->get(); 23 | } 24 | 25 | public function wellknown(ProviderRepository $providerRepository) 26 | { 27 | return $providerRepository->wellknown(); 28 | } 29 | 30 | public function webfinger(ProviderRepository $providerRepository, Request $request) 31 | { 32 | $result = [ 33 | "links" => [ 34 | [ 35 | "rel" => "http://openid.net/specs/connect/1.0/issuer", 36 | "href" => url('/') 37 | ] 38 | ] 39 | ]; 40 | 41 | if ($request->input('subject')) { 42 | $result['subject'] = $request->input('subject'); 43 | } 44 | 45 | return $result; 46 | } 47 | 48 | public static function pem2der($pem_data) 49 | { 50 | $begin = "CERTIFICATE-----"; 51 | $end = "-----END"; 52 | $pem_data = substr($pem_data, strpos($pem_data, $begin) + strlen($begin)); 53 | $pem_data = substr($pem_data, 0, strpos($pem_data, $end)); 54 | $der = base64_decode($pem_data); 55 | return $der; 56 | } 57 | 58 | public function jwks(ProviderRepository $providerRepository) 59 | { 60 | /** 61 | * @var CryptKey $crypt 62 | */ 63 | $crypt = resolve(KeyRepository::class)->getPublicKey(); 64 | 65 | $result = [ 66 | 'alg' => 'RS256', 67 | 'kty' => 'RSA', 68 | 'use' => 'sig', 69 | 'kid' => $crypt->kid ?? 1 70 | ]; 71 | 72 | if (!empty($crypt->x509)) { 73 | $key = $crypt->x509; 74 | $key = str_replace(array('-----BEGIN CERTIFICATE-----', '-----END CERTIFICATE-----', "\r", "\n", " "), "", $key); 75 | $keyForParsing = "-----BEGIN CERTIFICATE-----\n" . chunk_split($key, 64, "\n") . "-----END CERTIFICATE-----\n"; 76 | 77 | $pkey = openssl_pkey_get_details(openssl_pkey_get_public(openssl_x509_read($keyForParsing))); 78 | 79 | // Do not use x5c and x5t for now 80 | // $result['x5c'] = [ 81 | // self::pem2der($this->toWebSafe($pkey)) 82 | // ]; 83 | // $result['x5t'] = [ 84 | // $this->base64WebSafe(openssl_x509_fingerprint($keyForParsing, 'sha1', true)) 85 | // ]; 86 | 87 | } else { 88 | $pkey = openssl_pkey_get_details(openssl_pkey_get_public($crypt->getKeyContents())); 89 | } 90 | 91 | $result['n'] = $this->base64WebSafe($pkey['rsa']['n']); 92 | $result['e'] = $this->base64WebSafe($pkey['rsa']['e']); 93 | 94 | return [ 95 | 'keys' => [ 96 | $result 97 | ] 98 | ]; 99 | } 100 | 101 | public function update(Request $request, ProviderRepository $providerRepository) 102 | { 103 | return $providerRepository->update($request); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/ProviderRepository.php: -------------------------------------------------------------------------------- 1 | input('token'); 15 | $tokenTypeHint = $request->input('token_type_hint'); 16 | 17 | if (!empty($tokenTypeHint) || $tokenTypeHint != 'access_token') { 18 | return response([ 19 | 'error' => 'unsupported_token_type' 20 | ], 400); 21 | } 22 | 23 | $token = $jwt->parse($tokenString); 24 | 25 | try { 26 | $validator->ensureValidity($token); 27 | } catch (OAuthServerException $e) { 28 | return response(null, 200); 29 | } 30 | 31 | $tokenEloquent = $tokens->find($token->getClaim('jti')); 32 | 33 | $tokenEloquent->revoke(); 34 | 35 | return response(null, 200); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/RouteRegistrar.php: -------------------------------------------------------------------------------- 1 | router = $router; 31 | } 32 | 33 | public function forAuthorization() 34 | { 35 | $this->router->group(['middleware' => ['web']], function ($router) { 36 | $router->get('/authorize', [ 37 | 'uses' => '\Idaas\Passport\Http\Controllers\AuthorizationController@authorize', 38 | ])->name('oauth.authorize'); 39 | 40 | 41 | $router->get('/logout', [ 42 | 'uses' => '\Idaas\Passport\SessionManagementController@logout', 43 | ])->name('oidc.logout'); 44 | }); 45 | } 46 | 47 | public function forUserinfo() 48 | { 49 | $this->router->group([], function ($router) { 50 | $this->router->match(['get', 'post'], '/userinfo', [ 51 | 'uses' => '\Idaas\Passport\UserInfoController@userinfo', 52 | ])->name('oidc.userinfo'); 53 | }); 54 | } 55 | 56 | public function forIntrospect() 57 | { 58 | $this->router->group([], function ($router) { 59 | $this->router->post('/introspect', [ 60 | 'uses' => '\Idaas\Passport\IntrospectionController@introspect', 61 | ])->name('oauth.introspect'); 62 | 63 | $this->router->post('/revoke', [ 64 | 'uses' => '\Idaas\Passport\RevokeController@index', 65 | ])->name('oauth.revoke'); 66 | }); 67 | } 68 | 69 | public function forWellKnown() 70 | { 71 | $this->router->group([], function ($router) { 72 | $router->get('/.well-known/openid-configuration', [ 73 | 'uses' => '\Idaas\Passport\ProviderController@wellknown', 74 | ])->name('oidc.configuration'); 75 | 76 | $router->get('/.well-known/jwks.json', [ 77 | 'uses' => '\Idaas\Passport\ProviderController@jwks', 78 | ])->name('oidc.jwks'); 79 | 80 | $router->get('/.well-known/webfinger', [ 81 | 'uses' => '\Idaas\Passport\ProviderController@webfinger', 82 | ])->name('oidc.webfinger'); 83 | }); 84 | } 85 | 86 | public function forManagement() 87 | { 88 | $this->router->group(['middleware' => ['api']], function ($router) { 89 | $router->get('/oidc/provider', [ 90 | 'uses' => '\Idaas\Passport\ProviderController@index', 91 | ]); 92 | 93 | $router->put('/oidc/provider', [ 94 | 'uses' => '\Idaas\Passport\ProviderController@update', 95 | ]); 96 | }); 97 | } 98 | 99 | public function forOIDCClients() 100 | { 101 | $this->router->group(['middleware' => ['api']], function ($router) { 102 | $router->get('/connect/register', [ 103 | 'uses' => '\Idaas\Passport\ClientController@forUser', 104 | ])->name('oidc.manage.client.list'); 105 | 106 | $router->post('/connect/register', [ 107 | 'uses' => '\Idaas\Passport\ClientController@store', 108 | ])->name('oidc.manage.client.create'); 109 | 110 | // Not in the specs, yet useful 111 | $router->get('/connect/register/{client_id}', [ 112 | 'uses' => '\Idaas\Passport\ClientController@get', 113 | ])->name('oidc.manage.client.get'); 114 | 115 | $router->put('/connect/register/{client_id}', [ 116 | 'uses' => '\Idaas\Passport\ClientController@update', 117 | ])->name('oidc.manage.client.replace'); 118 | 119 | $router->delete('/connect/register/{client_id}', [ 120 | 'uses' => '\Idaas\Passport\ClientController@destroy', 121 | ])->name('oidc.manage.client.delete'); 122 | }); 123 | } 124 | 125 | /** 126 | * Register the routes for retrieving and issuing access tokens. 127 | * 128 | * @return void 129 | */ 130 | public function forAccessTokens() 131 | { 132 | $this->router->post('/token', [ 133 | 'uses' => '\Idaas\Passport\Http\Controllers\AccessTokenController@issueToken', 134 | 'middleware' => 'throttle', 135 | ])->name('oauth.token'); 136 | 137 | $this->router->group(['middleware' => ['web', 'auth']], function ($router) { 138 | $router->delete('/tokens/{token_id}', [ 139 | 'uses' => 'AuthorizedAccessTokenController@destroy', 140 | ]); 141 | }); 142 | } 143 | 144 | public function all() 145 | { 146 | $this->forAuthorization(); 147 | $this->forUserinfo(); 148 | $this->forIntrospect(); 149 | $this->forManagement(); 150 | $this->forOIDCClients(); 151 | $this->forAccessTokens(); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/SessionManagementController.php: -------------------------------------------------------------------------------- 1 | input('post_logout_redirect_uri'); 14 | $state = $request->input('state'); 15 | 16 | //TODO: make json column. Index and search through all. A lot faster. 17 | $valid = true; 18 | 19 | if (!empty($redirectUri)) { 20 | $valid = Client::select('post_logout_redirect_uris')->whereNotNull('post_logout_redirect_uris')->get()->map(function ($client) { 21 | return $client->post_logout_redirect_uris; 22 | })->collapse()->contains($redirectUri); 23 | } 24 | 25 | if (!$valid) { 26 | $redirectUri = null; 27 | $state = null; 28 | } 29 | 30 | return resolve(PassportConfig::class)->doLogoutResponse($request, $valid, $redirectUri, $state); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/TokenCache.php: -------------------------------------------------------------------------------- 1 | createRequest($request); 22 | 23 | /* @var $userinfo \Idaas\OpenID\UserInfo */ 24 | $userinfo = resolve(UserInfo::class); 25 | 26 | return $userinfo->respondToUserInfoRequest($psr, new Response()); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/routes/web.php: -------------------------------------------------------------------------------- 1 | '\Laravel\Passport\Http\Controllers', 7 | ], function ($router) { 8 | // Configurations endpoint needs to be registered without a prefix. 9 | $router->get('/.well-known/openid-configuration', [ 10 | 'uses' => '\Idaas\Passport\ProviderController@wellknown', 11 | ])->name('oidc.configuration'); 12 | 13 | $router->get('/.well-known/jwks.json', [ 14 | 'uses' => '\Idaas\Passport\ProviderController@jwks', 15 | ])->name('oidc.jwks'); 16 | 17 | $router->get('/.well-known/webfinger', [ 18 | 'uses' => '\Idaas\Passport\ProviderController@webfinger', 19 | ])->name('oidc.webfinger'); 20 | }); 21 | 22 | Route::group([ 23 | 'namespace' => '\Laravel\Passport\Http\Controllers', 24 | 'prefix' => config('passport.path', 'oauth'), 25 | ], function () { 26 | Route::post('/token', [ 27 | 'uses' => '\Idaas\Passport\Http\Controllers\AccessTokenController@issueToken', 28 | 'middleware' => 'throttle', 29 | ])->name('oauth.token'); 30 | 31 | Route::group(['middleware' => ['web', 'auth']], function ($router) { 32 | $router->delete('/tokens/{token_id}', [ 33 | 'uses' => 'AuthorizedAccessTokenController@destroy', 34 | ]); 35 | }); 36 | 37 | Route::group(['middleware' => ['web', 'auth:web']], function ($router) { 38 | $router->get('/authorize', [ 39 | 'uses' => '\Idaas\Passport\Http\Controllers\AuthorizationController@authorize', 40 | ])->name('oauth.authorize'); 41 | 42 | $router->get('/logout', [ 43 | 'uses' => '\Idaas\Passport\SessionManagementController@logout', 44 | ])->name('oidc.logout'); 45 | }); 46 | 47 | Route::group([], function ($router) { 48 | $router->match(['get', 'post'], '/userinfo', [ 49 | 'uses' => '\Idaas\Passport\UserInfoController@userinfo', 50 | ])->name('oidc.userinfo'); 51 | }); 52 | 53 | Route::group([], function ($router) { 54 | $router->post('/introspect', [ 55 | 'uses' => '\Idaas\Passport\IntrospectionController@introspect', 56 | ])->name('oauth.introspect'); 57 | 58 | $router->post('/revoke', [ 59 | 'uses' => '\Idaas\Passport\RevokeController@index', 60 | ])->name('oauth.revoke'); 61 | }); 62 | 63 | Route::group(['middleware' => ['api']], function ($router) { 64 | $router->get('/oidc/provider', [ 65 | 'uses' => '\Idaas\Passport\ProviderController@index', 66 | ]); 67 | 68 | $router->put('/oidc/provider', [ 69 | 'uses' => '\Idaas\Passport\ProviderController@update', 70 | ]); 71 | }); 72 | 73 | // For OIDCClient 74 | Route::group(['middleware' => ['api']], function ($router) { 75 | $router->get('/connect/register', [ 76 | 'uses' => '\Idaas\Passport\ClientController@forUser', 77 | ])->name('oidc.manage.client.list'); 78 | 79 | $router->post('/connect/register', [ 80 | 'uses' => '\Idaas\Passport\ClientController@store', 81 | ])->name('oidc.manage.client.create'); 82 | 83 | // Not in the specs, yet useful 84 | $router->get('/connect/register/{client_id}', [ 85 | 'uses' => '\Idaas\Passport\ClientController@get', 86 | ])->name('oidc.manage.client.get'); 87 | 88 | $router->put('/connect/register/{client_id}', [ 89 | 'uses' => '\Idaas\Passport\ClientController@update', 90 | ])->name('oidc.manage.client.replace'); 91 | 92 | $router->delete('/connect/register/{client_id}', [ 93 | 'uses' => '\Idaas\Passport\ClientController@destroy', 94 | ])->name('oidc.manage.client.delete'); 95 | }); 96 | }); -------------------------------------------------------------------------------- /tests/AccessTokenEntity.php: -------------------------------------------------------------------------------- 1 | afterApplicationCreated(function () { 51 | $this->app->register(\Idaas\Passport\PassportServiceProvider::class); 52 | }); 53 | 54 | Passport::loadKeysFrom(__DIR__ . '/files'); 55 | parent::setUp(); 56 | 57 | chmod(__DIR__ . '/../vendor/laravel/passport/tests/Feature/../keys/oauth-private.key', 0660); 58 | chmod(__DIR__ . '/../vendor/laravel/passport/tests/Feature/../keys/oauth-public.key', 0660); 59 | } 60 | 61 | protected function tearDown(): void 62 | { 63 | m::close(); 64 | } 65 | 66 | public function testAuthorizationBasic() 67 | { 68 | Passport::tokensCan([ 69 | 'scope-1' => 'description', 70 | ]); 71 | 72 | $clients = m::mock(ClientRepository::class); 73 | $clients->shouldReceive('find')->andReturn( 74 | $client = new Client([]) 75 | ); 76 | 77 | $client = m::mock(ClientEntityInterface::class); 78 | $client->shouldReceive('getRedirectUri')->andReturn('https://test123.nl'); 79 | $client->shouldReceive('isConfidential')->andReturn(false); 80 | $client->shouldReceive('getIdentifier')->andReturn('123'); 81 | 82 | $clientRepository = m::mock(IdaasClientRepository::class); 83 | $clientRepository->shouldReceive('getClientEntity')->andReturn($client); 84 | 85 | $scopeEntity = new Scope('openid'); 86 | 87 | $scopeRepository = m::mock(ScopeRepositoryInterface::class); 88 | $scopeRepository->shouldReceive('getScopeEntityByIdentifier')->andReturn( 89 | $scopeEntity 90 | ); 91 | $scopeRepository->shouldReceive('finalizeScopes')->andReturn( 92 | [$scopeEntity] 93 | ); 94 | 95 | $tokenRepository = m::mock(LeagueAccessTokenRepositoryInterface::class); 96 | $tokenRepository->shouldReceive('getNewToken')->andReturn( 97 | new AccessToken('test', [$scopeEntity], $client) 98 | ); 99 | $tokenRepository->shouldReceive('persistNewAccessToken')->andReturn(true); 100 | 101 | $server = new AuthorizationServer( 102 | $clientRepository, 103 | $tokenRepository, 104 | $scopeRepository, 105 | (new KeyRepository())->getPrivateKey(), 106 | "test", 107 | new BearerTokenResponse() 108 | ); 109 | 110 | $claimRepository = m::mock(ClaimRepository::class); 111 | $claimRepository->shouldReceive('claimsRequestToEntities')->andReturn([]); 112 | 113 | $authCodeRepository = m::mock(AuthCodeRepository::class); 114 | $authCodeRepository->shouldReceive('getNewAuthCode')->andReturn(new AuthCode()); 115 | $authCodeRepository->shouldReceive('persistNewAuthCode')->andReturn(true); 116 | $authCodeRepository->shouldReceive('isAuthCodeRevoked')->andReturn(false); 117 | $authCodeRepository->shouldReceive('revokeAuthCode')->andReturn(true); 118 | 119 | $refreshTokenRepository = m::mock(RefreshTokenRepository::class); 120 | $refreshTokenRepository->shouldReceive('getNewRefreshToken')->andReturn(new RefreshToken()); 121 | $refreshTokenRepository->shouldReceive('persistNewRefreshToken')->andReturn(true); 122 | 123 | $grant = new AuthCodeGrant( 124 | $authCodeRepository, 125 | $refreshTokenRepository, 126 | $claimRepository, 127 | new Session(), 128 | new DateInterval('P1Y'), 129 | new DateInterval('P1Y') 130 | ); 131 | 132 | $grant->setRefreshTokenTTL(new DateInterval('P1Y')); 133 | $grant->setIssuer('https://issuer.example.com'); 134 | $grant->disableRequireCodeChallengeForPublicClients(); 135 | 136 | $server->enableGrantType( 137 | $grant, 138 | new DateInterval('P1Y') 139 | ); 140 | 141 | $response = m::mock(AuthorizationViewResponse::class); 142 | $guard = m::mock(\Illuminate\Contracts\Auth\StatefulGuard::class); 143 | 144 | $controller = new AuthorizationController( 145 | $server, 146 | $guard, 147 | $response 148 | ); 149 | 150 | $authenticationRequest = m::mock(AuthenticationRequest::class); 151 | 152 | $client = m::mock(ClientEntityInterface::class); 153 | $client->shouldReceive('getIdentifier')->andReturn('test'); 154 | $client->shouldReceive('isConfidential')->andReturn(true); 155 | 156 | $authenticationRequest->shouldReceive('getClient')->andReturn( 157 | $client 158 | ); 159 | 160 | $user = m::mock(Authenticatable::class); 161 | $user->shouldReceive('getKey')->andReturn('test'); 162 | $user->shouldReceive('getAuthIdentifier')->andReturn('test'); 163 | 164 | $request = m::mock(Request::class); 165 | $request->shouldReceive('user')->andReturn($user); 166 | 167 | $tokens = m::mock(TokenRepository::class); 168 | $token = m::mock(Token::class); 169 | $token->shouldReceive('getAttribute')->andReturn([]); 170 | $tokens->shouldReceive('findValidToken')->andReturn( 171 | $token 172 | ); 173 | 174 | $serverRequest = m::mock(ServerRequestInterface::class); 175 | $serverRequest->shouldReceive('getQueryParams')->andReturn([ 176 | 'response_type' => 'code', 177 | 'client_id' => '123', 178 | 'scope' => 'openid', 179 | 'redirect_uri' => 'https://test123.nl' 180 | ]); 181 | $serverRequest->shouldReceive('getServerParams')->andReturn([]); 182 | $serverRequest->shouldReceive('hasHeader')->andReturn(false); 183 | 184 | /** */ 185 | $result = $controller->authorize( 186 | $serverRequest, 187 | $request, 188 | $clients, 189 | $tokens 190 | ); 191 | 192 | $location = $result->headers->get('Location'); 193 | 194 | $this->assertNotNull($location); 195 | $parsed = parse_url($location); 196 | 197 | $this->assertArrayHasKey('query', $parsed); 198 | parse_str($parsed['query'], $parseStr); 199 | $this->assertArrayHasKey('code', $parseStr); 200 | 201 | $controller = new AccessTokenController($server, $tokens, new Parser(new JoseEncoder())); 202 | 203 | $serverRequest = m::mock(ServerRequestInterface::class); 204 | $serverRequest->shouldReceive('getParsedBody')->andReturn([ 205 | 'grant_type' => 'authorization_code', 206 | 'code' => $parseStr['code'], 207 | 'client_id' => '123', 208 | 'redirect_uri' => 'https://test123.nl' 209 | ]); 210 | 211 | $serverRequest->shouldReceive('getServerParams')->andReturn([]); 212 | $serverRequest->shouldReceive('hasHeader')->andReturn(false); 213 | 214 | $response = $controller->issueToken($serverRequest); 215 | 216 | $content = $response->content(); 217 | 218 | $this->assertJson($content); 219 | 220 | $json = json_decode($content, true); 221 | 222 | $this->assertArrayHasKey('id_token', $json); 223 | $this->assertArrayHasKey('access_token', $json); 224 | $this->assertArrayHasKey('token_type', $json); 225 | $this->assertArrayHasKey('expires_in', $json); 226 | $this->assertArrayHasKey('refresh_token', $json); 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /tests/Bootstrap.php: -------------------------------------------------------------------------------- 1 | wget http://getcomposer.org/composer.phar 7 | > php composer.phar install 8 | MSG; 9 | 10 | exit($message); 11 | } 12 | 13 | chmod(dirname(__FILE__) . '/files/oauth-private.key', 0600); 14 | -------------------------------------------------------------------------------- /tests/UserinfoControllerTest.php: -------------------------------------------------------------------------------- 1 | afterApplicationCreated(function () { 38 | $this->app->register(\Idaas\Passport\PassportServiceProvider::class); 39 | }); 40 | 41 | parent::setUp(); 42 | 43 | $accessTokenRepository = m::mock(\Idaas\Passport\Bridge\AccessTokenRepository::class); 44 | $accessTokenRepository->shouldReceive('isAccessTokenRevoked')->andReturn(false); 45 | $accessTokenRepository->shouldReceive('getAccessToken')->andReturn( 46 | new AccessTokenEntity('123', [], m::mock(ClientEntityInterface::class)) 47 | ); 48 | 49 | $this->app->instance( 50 | \Idaas\Passport\Bridge\AccessTokenRepository::class, 51 | $accessTokenRepository 52 | ); 53 | 54 | $this->withFactories(__DIR__ . '/../../database/factories'); 55 | 56 | $this->artisan('migrate:fresh'); 57 | 58 | Passport::routes(function ($router) { 59 | $router->all(); 60 | $router->forUserinfo(); 61 | $router->forOIDCClients(); 62 | }); 63 | 64 | $this->artisan('passport:keys'); 65 | 66 | chmod(__DIR__ . '/../vendor/laravel/passport/tests/Feature/../keys/oauth-private.key', 0660); 67 | chmod(__DIR__ . '/../vendor/laravel/passport/tests/Feature/../keys/oauth-public.key', 0660); 68 | 69 | } 70 | 71 | protected function getEnvironmentSetUp($app) 72 | { 73 | parent::getEnvironmentSetUp($app); 74 | 75 | $config = $app->make(Repository::class); 76 | $config->set('database.connections.forge', [ 77 | 'driver' => 'sqlite', 78 | 'database' => '/tmp/database.sqlite' 79 | ]); 80 | 81 | $config->set('auth.providers.users.model', TestUser::class); 82 | } 83 | 84 | protected function tearDown(): void 85 | { 86 | m::close(); 87 | } 88 | 89 | public function testUserinfoBasic() 90 | { 91 | $keyRepository = new KeyRepository(); 92 | 93 | $config = Configuration::forAsymmetricSigner( 94 | new Sha256(), 95 | InMemory::plainText($keyRepository->getPrivateKey()->getKeyContents()), 96 | InMemory::plainText($keyRepository->getPrivateKey()->getKeyContents()) 97 | ); 98 | 99 | $token = $config->builder() 100 | ->permittedFor('client') 101 | ->identifiedBy('1234') 102 | ->issuedAt(new DateTimeImmutable()) 103 | ->canOnlyBeUsedAfter(new DateTimeImmutable()) 104 | ->expiresAt(DateTimeImmutable::createFromMutable(new DateTime("+7 day"))) 105 | ->relatedTo('user-id-1234') 106 | ->withClaim('scopes', [ 107 | new Scope('openid') 108 | ]) 109 | ->withClaim('claims', ['claim1']) 110 | ->getToken($config->signer(), $config->signingKey()) 111 | ->toString(); 112 | 113 | $result = $this->get('/oauth/userinfo', [ 114 | 'Authorization' => $token 115 | ]); 116 | 117 | $result->assertStatus(200); 118 | 119 | $result = $this->get('/oauth/connect/register', [ 120 | 'Authorization' => $token 121 | ]); 122 | 123 | $result->assertStatus(200); 124 | } 125 | } 126 | 127 | class TestUser extends User 128 | { 129 | use HasApiTokens; 130 | 131 | protected $table = 'users'; 132 | 133 | public function findForPassport($identifier) 134 | { 135 | return new TestUser([]); 136 | } 137 | 138 | public function getAuthIdentifier() 139 | { 140 | return "test"; 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /tests/files/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/limosa-io/laravel-openid-connect-server/91beee656b50fa13130d5047cc67b7b6dd0ef60c/tests/files/.gitkeep -------------------------------------------------------------------------------- /tests/files/oauth-private.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIJKQIBAAKCAgEAtdpanNlTsPgEH+Cczk/LGdQNfOS+8AeAVdNvemfVCij+SK9n 3 | TF4u9MvqeTCba6WBZke6V/gXVJdObecpKgqwvTpvHy4ofFnQKNftIxbM00tGr1VV 4 | j1QUiBFpNH9zEjNUWKKS77vLbVJyHKjrv0xkZ26Z9rgjuMJyQS+ZhDWIOxJV7Bzn 5 | B2BtVAMqtGLGiqvH9E5EVMfShwF723Jm3XWhMzXasN1LIusNWPlgIKfu7fY4I3yv 6 | 7Tf9T0X/nnoPrNf/zeAru/LcW3bxF5+pNSLCkKWLnUUx7F3V0Y6zwu+O5yO29Mnt 7 | FCD6d1aGy7wpj9nG+hf4S8QavTsbX8CH/sXN5KdcfDSjVroHp7424hNVanL4Coyv 8 | jHTfEqmloueyG8WxRgNHWIBqTcQZq0FY73+ZLYBqHbDX+KLbERnX//FB+4Dokt+G 9 | Os1jfZQJAGMZyrikq8xTpXzGWPB8ndISgqzRUStU/M0DZZcLmCVpuxkQRTRyi8QJ 10 | zUxW+2l2OSt4BVVqGSzxdPWpoWgFr14Fg4EwVcDq92CJbrbVPGKzQ9dYD7H7O3YT 11 | XnMVJj5lR5xmYcoCk9BWLGyMOUNxyZDbR2L4ICM4H0ZedkuZocs+Bxn0ozzlVaoH 12 | u7cWyX1HNirCV3p+lXuiiXSyQ4udyFVdTJlbgUr0peUSPWC75vN04kawOuUCAwEA 13 | AQKCAgEAipOzWzUNXp0Q343VUC5kEfGSuarCibzdogl2OOvo304vwAnSCNL4q1i8 14 | +877kMNVYTCloqWOecq3XG674qq01e4ygas19NSoGIe60HNucFE7sx6vDYLABpRl 15 | /Dhm4ua0jrqMiB0uPseF56sdwvAezubscqMNrZyXXm88aBA3GPS2/y9jKi7kARJM 16 | t5tRuph+zf/aeFSxbGnIDYkXNAmQZqrSVNa9jAVcbYHTK+9s1m87hmdc1MNGx+MK 17 | kKRpT4hmCWebTCUcoKJ5xEvJsJElfP557sWs7nbvGjrJZ3IQDrkbkVxSynT7CHeq 18 | TpS7g0AsLaaYcnwk+DCTgpr/xzjK1PmZNl/YBA0ZNCTQPLWmfMfaE4RK8fZLRw9R 19 | ZaS9qropjZgpbDwvNY467tY8HXf4maf8f0JF0NHW5hn/ocDM6HQUn3tY/OVeBt0d 20 | LYGe3Npb3lzHxc97ZSbY0PsqyOKJlVQ9T6oDBIKFyusRFVE4vIy08foe4BFRN69U 21 | CsxFCoP07Q5CHuPt2bpYaOgkKPJWF4dNnG5e10leS3sKy+ZHw5Qm7nVsqwYN0q5z 22 | 4ewdOcyq0EBadRI7ApYPwStQ56pf9LLnb/33BFnop6p2NMVoGubXNDNAYX97bK/F 23 | wXBPpm1NDjLKjlX37IoMMWty4TLi+/xvdMCMn/RYWGfEL5cnwF0CggEBAPCvpxhW 24 | jLzDhsyOJj9MVpjcKMXqZ22Iiu8atx8dWJkUDwUQGqZwmC7P7rNfo0VfFEfT7veA 25 | HpCw4bOlFvY3ov9T4ECtbOe63L2gjMt1wQxAsgQIRHgeIzHJ4OS5ZMPKjDYbhZBQ 26 | XwZcoN+m0D9G0grEpUANLHJ//wuwTWKBfPwLD/ni5j93r41fNzwLwKGkhwhvSjZa 27 | pCnq9JkQGmhfziOwfj0w1Jh0ORWQ6RzmSgPLhspmtTbrud28wwhKuBH3ihWt8qIw 28 | nMhQEtYgbItDLCbatrkVmfvtguH55PmSLygIroUJ8j/Y/9x1MWp1yB3maba4fOaH 29 | /wfWOZIo4JJsUwMCggEBAMFsad5B5D6OKGhMsAyXE79rFncX8xRHsY8vJ5qSvtao 30 | uqrUjDnDfL+XP0c5bmhR7SJO3M+YT7nNq6e4rPPPLfhkWzDVmBjvWecNVTTHJMdg 31 | H1J/RaYfFbtDj1ydhhCWPjcq9aTQ7aepd/EybB98SzKnCH2drCSDvR+Zxlz+btY2 32 | wWCfFJ00+drYJ57DiZT7f+p2HGPO9NQ7G9XOH6HPxsGyz2hYqn407VJ136Yb55mo 33 | AkczpnKXMy7OLtgmvi60sdJILAI99t6/C2WXkQ5Bbb3Y5zIVrVRZNWhh+VthUqH9 34 | HVNniKFhlXxgTAnBqyeEdJ1bavYdsRbBFWxDfD7oYfcCggEBAK5fE7fWPHAlV+uq 35 | sJr7O2HTXtbEU9pPKhjAShsIr793gMoyOJyMxjRkAJODnk4L7C19pYs6DurUuy/j 36 | Uxdeh4Bjy/wPqO7ZukLHpjlhPlMaK94v/yrm6qiPkZcjmZUfkSVCGynKkI2TXhmq 37 | SQUO0e1rOi2FXcY2ZRiayfWryljvystHT3h7xXuul92wxgZVpF7AAgwO7xBPYJXj 38 | zL4Bat8AZvT+A4fP6+tr6Pc/ADBvq1/HuVHoxeeXcdT4DRM3O5spQreucGfPW1Y2 39 | d20NZVdg7TB6Le0OzdvZp8IH3pIqeAV6hz8qf3PAk+SFIE5JOB1g21xI+sM6uMrj 40 | Qo/SWvECggEAfgLrVekHkdl7abfMWedy7hoOILhxnJy4iNfnrUQ9IAv80J54XImH 41 | FCqtBvHbhAsbumY2ZOiNyDa9xh8CTjSDdnerET2WQWht4aFGCYRT6bAtt8lCzt8h 42 | gUaad3QIi3XWQoqyThXvPnOhuHlpB3oqY21+kpfONlu9MoE0QhFglB5IDXKWM3T3 43 | 8iMwbLZnQ7A1vcIE7OE/j6SSldAbu4dprXeYuBpLuL0yf65bbhRv5iMWmNyFa9Il 44 | fjDFQ/y5miQOVTi/sxY7ZxJ/kkgUMH1fyXct5+tHaNRVfxOVKHtXIkGMgXHKCZlR 45 | kh4Ka7DImI4qivmrzXu6i2BvXID6yOEMMQKCAQAtCX42FUj3wKTa0DPUooFehRRj 46 | itQ+QUERRFRPb3/j/xRDKFWYl9iXprcfs0VFs659E+fSIaAXoAboSdPEwI1jnWtf 47 | WB/TLd1y8MW1ulJvxcaYR4mB3rXbUxVl5q3piKrSS29xdP0FU1y+05mK4KCnF9rV 48 | mulIkYMKrJpuwEPEl8oS2WXWYW3yR0stgrFHklyMh0vADrlnd2/hQ/91te+L9E+D 49 | EH5ysSAEVk86w2NVWmxVAvccDYTyCdmTE26wGy4id4TERViwNXhVcZFVAV8F+S/N 50 | hlhUIsBJqeQ9kehfnlHH569SBI7+z64TJdJnMEmz2p8sXoHRzNI42Bj269P+ 51 | -----END RSA PRIVATE KEY----- -------------------------------------------------------------------------------- /tests/files/oauth-public.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAtdpanNlTsPgEH+Cczk/L 3 | GdQNfOS+8AeAVdNvemfVCij+SK9nTF4u9MvqeTCba6WBZke6V/gXVJdObecpKgqw 4 | vTpvHy4ofFnQKNftIxbM00tGr1VVj1QUiBFpNH9zEjNUWKKS77vLbVJyHKjrv0xk 5 | Z26Z9rgjuMJyQS+ZhDWIOxJV7BznB2BtVAMqtGLGiqvH9E5EVMfShwF723Jm3XWh 6 | MzXasN1LIusNWPlgIKfu7fY4I3yv7Tf9T0X/nnoPrNf/zeAru/LcW3bxF5+pNSLC 7 | kKWLnUUx7F3V0Y6zwu+O5yO29MntFCD6d1aGy7wpj9nG+hf4S8QavTsbX8CH/sXN 8 | 5KdcfDSjVroHp7424hNVanL4CoyvjHTfEqmloueyG8WxRgNHWIBqTcQZq0FY73+Z 9 | LYBqHbDX+KLbERnX//FB+4Dokt+GOs1jfZQJAGMZyrikq8xTpXzGWPB8ndISgqzR 10 | UStU/M0DZZcLmCVpuxkQRTRyi8QJzUxW+2l2OSt4BVVqGSzxdPWpoWgFr14Fg4Ew 11 | VcDq92CJbrbVPGKzQ9dYD7H7O3YTXnMVJj5lR5xmYcoCk9BWLGyMOUNxyZDbR2L4 12 | ICM4H0ZedkuZocs+Bxn0ozzlVaoHu7cWyX1HNirCV3p+lXuiiXSyQ4udyFVdTJlb 13 | gUr0peUSPWC75vN04kawOuUCAwEAAQ== 14 | -----END PUBLIC KEY----- --------------------------------------------------------------------------------