├── js ├── src │ ├── common │ │ ├── index.ts │ │ ├── models │ │ │ ├── Record.js │ │ │ ├── Token.js │ │ │ ├── Scope.js │ │ │ └── Client.js │ │ ├── utils │ │ │ └── randomString.js │ │ └── extend.js │ ├── admin │ │ ├── index.js │ │ ├── pages │ │ │ ├── TokensPage.js │ │ │ ├── ClientsPage.js │ │ │ ├── ScopesPage.js │ │ │ └── IndexPage.js │ │ └── components │ │ │ ├── SettingsPage.js │ │ │ ├── AddTokenModal.js │ │ │ ├── EditClientModal.js │ │ │ └── EditScopeModal.js │ └── forum │ │ ├── components │ │ ├── ScopeComponent.js │ │ ├── user │ │ │ └── AuthorizedPage.js │ │ └── oauth │ │ │ └── AuthorizePage.js │ │ └── index.js ├── admin.ts ├── forum.ts ├── webpack.config.js ├── tsconfig.json ├── package.json └── dist │ ├── forum.js │ ├── admin.js │ └── forum.js.map ├── less ├── forum │ ├── box.less │ ├── column.less │ ├── card.less │ └── oauth.less ├── forum.less └── admin.less ├── src ├── Models │ ├── Jwt.php │ ├── AccessToken.php │ ├── RefreshToken.php │ ├── AuthorizationCode.php │ ├── Record.php │ ├── Scope.php │ └── Client.php ├── Api │ ├── Controller │ │ ├── ListClientController.php │ │ ├── DeleteScopeController.php │ │ ├── DeleteClientController.php │ │ ├── CreateScopeController.php │ │ ├── DeleteAllRecordController.php │ │ ├── ListRecordController.php │ │ ├── ShowClientController.php │ │ ├── DeleteExpiredTokenController.php │ │ ├── ListScopeController.php │ │ ├── CreateClientController.php │ │ ├── DeleteUserRecordController.php │ │ ├── DeleteRecordController.php │ │ ├── UpdateScopeController.php │ │ ├── UpdateClientController.php │ │ └── CreateTokenController.php │ └── Serializer │ │ ├── ScopeUserSerializer.php │ │ ├── RecordSerializer.php │ │ ├── AccessTokenSerializer.php │ │ ├── ScopeSerializer.php │ │ ├── ClientPublicSerializer.php │ │ └── ClientSerializer.php ├── Middlewares │ ├── UnsetCsrfMiddleware.php │ ├── UserCredentialsMiddleware.php │ ├── ResourceScopeFieldsMiddleware.php │ └── ResourceScopeAuthMiddleware.php ├── Controllers │ ├── TokenController.php │ ├── ApiUserController.php │ ├── AuthorizeController.php │ └── AuthorizeFetchController.php ├── OAuth.php └── Storage.php ├── migrations ├── 2023_09_29_add_columns_to_oauth_scopes_table.php ├── 2023_09_29_add_columns_to_oauth_clients_table.php ├── 2024_04_18_add_columns_to_oauth_scopes_table.php ├── 2023_09_28_create_oauth_jwt_table.php ├── 2024_02_25_create_oauth_records_table.php ├── 2023_09_28_create_oauth_scopes_table.php ├── 2023_10_02_add_default_record_to_oauth_scopes_table.php ├── 2023_09_28_create_oauth_access_tokens_table.php ├── 2023_09_28_create_oauth_refresh_tokens_table.php ├── 2023_09_28_create_oauth_clients_table.php └── 2023_09_28_create_oauth_authorization_codes_table.php ├── README.md ├── LICENSE.md ├── locale ├── zh-Hans.yml └── en.yml ├── composer.json ├── docs ├── zh.md └── en.md └── extend.php /js/src/common/index.ts: -------------------------------------------------------------------------------- 1 | export { default as extend } from './extend'; 2 | -------------------------------------------------------------------------------- /js/admin.ts: -------------------------------------------------------------------------------- 1 | export * from './src/common'; 2 | export * from './src/admin'; 3 | -------------------------------------------------------------------------------- /js/forum.ts: -------------------------------------------------------------------------------- 1 | export * from './src/common'; 2 | export * from './src/forum'; 3 | -------------------------------------------------------------------------------- /js/webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require('flarum-webpack-config')(); 2 | -------------------------------------------------------------------------------- /js/src/common/models/Record.js: -------------------------------------------------------------------------------- 1 | import Model from 'flarum/common/Model'; 2 | 3 | export default class Record extends Model { 4 | client = Model.hasOne('client'); 5 | user_id = Model.attribute('user_id'); 6 | authorized_at = Model.attribute('authorized_at', Model.transformDate); 7 | } 8 | -------------------------------------------------------------------------------- /js/src/common/models/Token.js: -------------------------------------------------------------------------------- 1 | import Model from 'flarum/common/Model'; 2 | 3 | export default class Token extends Model { 4 | access_token = Model.attribute('access_token'); 5 | client_id = Model.attribute('client_id'); 6 | user_id = Model.attribute('user_id'); 7 | expires = Model.attribute('expires', Model.transformDate); 8 | scope = Model.attribute('scope'); 9 | } 10 | -------------------------------------------------------------------------------- /js/src/common/utils/randomString.js: -------------------------------------------------------------------------------- 1 | export function randomString(len) { 2 | len = len || 8; 3 | let $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; 4 | let maxPos = $chars.length; 5 | let str = ''; 6 | for (let i = 0; i < len; i++) { 7 | //0~32的整数 8 | str += $chars.charAt(Math.floor(Math.random() * (maxPos + 1))); 9 | } 10 | return str; 11 | } 12 | -------------------------------------------------------------------------------- /js/src/common/extend.js: -------------------------------------------------------------------------------- 1 | import Extend from 'flarum/common/extenders'; 2 | import Client from "./models/Client"; 3 | import Scope from "./models/Scope"; 4 | import Record from "./models/Record"; 5 | import Token from "./models/Token"; 6 | 7 | export default [ 8 | new Extend.Store() 9 | .add('oauth-clients', Client) 10 | .add('oauth-scopes', Scope) 11 | .add('oauth-records', Record) 12 | .add('oauth-tokens', Token) 13 | ]; 14 | -------------------------------------------------------------------------------- /less/forum/box.less: -------------------------------------------------------------------------------- 1 | .oauth-main.oauth-box { 2 | position: relative; 3 | width: 100%; 4 | max-width: 376px; 5 | margin: 0 auto; 6 | box-sizing: border-box; 7 | box-shadow: 0 0 15px 0 @shadow-color; 8 | border-radius: @border-radius; 9 | } 10 | 11 | .oauth-main.oauth-box::before { 12 | backdrop-filter: blur(20px); 13 | content: ''; 14 | position: absolute; 15 | width: 100%; 16 | height: 100%; 17 | background: hsla(0, 0%, 100%, .3); 18 | border-radius: @border-radius; 19 | } 20 | -------------------------------------------------------------------------------- /src/Models/Jwt.php: -------------------------------------------------------------------------------- 1 | { 4 | app.extensionData 5 | .for('foskym-oauth-center') 6 | .registerPage(SettingsPage) 7 | .registerPermission( 8 | { 9 | icon: 'fas fa-user-friends', 10 | label: app.translator.trans('foskym-oauth-center.admin.permission.use_oauth'), 11 | permission: 'foskym-oauth-center.use-oauth', 12 | }, 13 | 'view', 14 | 95 15 | ); 16 | }); 17 | -------------------------------------------------------------------------------- /js/src/common/models/Client.js: -------------------------------------------------------------------------------- 1 | import Model from 'flarum/common/Model'; 2 | 3 | export default class Client extends Model { 4 | client_id = Model.attribute('client_id'); 5 | client_secret = Model.attribute('client_secret'); 6 | redirect_uri = Model.attribute('redirect_uri'); 7 | grant_types = Model.attribute('grant_types'); 8 | scope = Model.attribute('scope'); 9 | user_id = Model.attribute('user_id'); 10 | client_name = Model.attribute('client_name'); 11 | client_icon = Model.attribute('client_icon'); 12 | client_desc = Model.attribute('client_desc'); 13 | client_home = Model.attribute('client_home'); 14 | } 15 | -------------------------------------------------------------------------------- /src/Models/Record.php: -------------------------------------------------------------------------------- 1 | belongsTo(Client::class, 'client_id', 'client_id'); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /less/forum/card.less: -------------------------------------------------------------------------------- 1 | .oauth-main.oauth-card { 2 | position: relative; 3 | margin: 0 auto; 4 | box-sizing: border-box; 5 | box-shadow: 0 0 15px 0 @shadow-color; 6 | border-radius: @border-radius; 7 | } 8 | 9 | .oauth-main.oauth-card::before { 10 | backdrop-filter: blur(20px); 11 | content: ''; 12 | position: absolute; 13 | width: 100%; 14 | height: 100%; 15 | background: hsla(0, 0%, 100%, .3); 16 | border-radius: 12px; 17 | } 18 | 19 | .oauth-card { 20 | .oauth-body { 21 | display: flex; 22 | flex-direction: row-reverse; 23 | justify-content: space-between; 24 | flex-wrap: wrap; 25 | border-radius: 0 0 @border-radius @border-radius; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /migrations/2023_09_29_add_columns_to_oauth_scopes_table.php: -------------------------------------------------------------------------------- 1 | ['string', 'length' => 500, 'default' => null, 'nullable' => true], 16 | 'scope_icon' => ['string', 'length' => 500, 'default' => null, 'nullable' => true], 17 | 'scope_desc' => ['string', 'length' => 1000, 'default' => null, 'nullable' => true], 18 | ]); 19 | -------------------------------------------------------------------------------- /src/Models/Scope.php: -------------------------------------------------------------------------------- 1 | get(); 22 | // return $this->where('resource_path', $path)->first(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Api/Controller/ListClientController.php: -------------------------------------------------------------------------------- 1 | assertAdmin(); 20 | 21 | return Client::all(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /js/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use Flarum's tsconfig as a starting point 3 | "extends": "flarum-tsconfig", 4 | // This will match all .ts, .tsx, .d.ts, .js, .jsx files in your `src` folder 5 | // and also tells your Typescript server to read core's global typings for 6 | // access to `dayjs` and `$` in the global namespace. 7 | "include": [ 8 | "src/**/*", 9 | "../vendor/*/*/js/dist-typings/@types/**/*", 10 | // 11 | // 12 | "@types/**/*" 13 | ], 14 | "compilerOptions": { 15 | // This will output typings to `dist-typings` 16 | "declarationDir": "./dist-typings", 17 | "baseUrl": ".", 18 | "paths": { 19 | "flarum/*": ["../vendor/flarum/core/js/dist-typings/*"], 20 | // 21 | // 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /migrations/2023_09_29_add_columns_to_oauth_clients_table.php: -------------------------------------------------------------------------------- 1 | ['string', 'length' => 500, 'default' => null, 'nullable' => true], 16 | 'client_icon' => ['string', 'length' => 500, 'default' => null, 'nullable' => true], 17 | 'client_desc' => ['string', 'length' => 1000, 'default' => null, 'nullable' => true], 18 | 'client_home' => ['string', 'length' => 200, 'default' => null, 'nullable' => true], 19 | ]); 20 | -------------------------------------------------------------------------------- /src/Api/Controller/DeleteScopeController.php: -------------------------------------------------------------------------------- 1 | getQueryParams(), 'id'); 18 | RequestUtil::getActor($request) 19 | ->assertAdmin(); 20 | 21 | $scope = Scope::find($id); 22 | 23 | $scope->delete(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Api/Controller/DeleteClientController.php: -------------------------------------------------------------------------------- 1 | getQueryParams(), 'id'); 19 | RequestUtil::getActor($request) 20 | ->assertAdmin(); 21 | 22 | $client = Client::find($id); 23 | 24 | $client->delete(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /migrations/2024_04_18_add_columns_to_oauth_scopes_table.php: -------------------------------------------------------------------------------- 1 | function (Builder $schema) { 16 | if (!$schema->hasTable('oauth_scopes')) { 17 | return; 18 | } 19 | 20 | if (!$schema->hasColumn('oauth_scopes', 'visible_fields')) { 21 | $schema->table('oauth_scopes', function (Blueprint $table) { 22 | $table->string('visible_fields', 4000)->nullable()->after('method'); 23 | }); 24 | } 25 | }, 26 | 'down' => function (Builder $schema) { 27 | 28 | }, 29 | ]; 30 | -------------------------------------------------------------------------------- /src/Api/Controller/CreateScopeController.php: -------------------------------------------------------------------------------- 1 | assertAdmin(); 20 | 21 | $attributes = Arr::get($request->getParsedBody(), 'data.attributes'); 22 | 23 | return Scope::create([ 24 | 'scope' => Arr::get($attributes, 'scope'), 25 | ]); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Models/Client.php: -------------------------------------------------------------------------------- 1 | client_id = $client_id; 25 | $client->client_secret = $client_secret; 26 | $client->user_id = $user_id; 27 | 28 | return $client; 29 | } 30 | 31 | public function record() 32 | { 33 | return $this->hasMany(Record::class, 'client_id', 'client_id'); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /migrations/2023_09_28_create_oauth_jwt_table.php: -------------------------------------------------------------------------------- 1 | function (Builder $schema) { 16 | if ($schema->hasTable('oauth_jwt')) { 17 | return; 18 | } 19 | $schema->create('oauth_jwt', function (Blueprint $table) { 20 | $table->increments('id'); 21 | $table->string('client_id', 80); 22 | $table->string('subject', 80)->nullable(); 23 | $table->string('public_key', 2000); 24 | }); 25 | }, 26 | 'down' => function (Builder $schema) { 27 | $schema->dropIfExists('oauth_jwt'); 28 | }, 29 | ]; 30 | -------------------------------------------------------------------------------- /migrations/2024_02_25_create_oauth_records_table.php: -------------------------------------------------------------------------------- 1 | function (Builder $schema) { 16 | if ($schema->hasTable('oauth_records')) { 17 | return; 18 | } 19 | $schema->create('oauth_records', function (Blueprint $table) { 20 | $table->increments('id'); 21 | $table->string('client_id', 80); 22 | $table->string('user_id', 80)->nullable(); 23 | $table->timestamp('authorized_at'); 24 | }); 25 | }, 26 | 'down' => function (Builder $schema) { 27 | $schema->dropIfExists('oauth_records'); 28 | }, 29 | ]; 30 | -------------------------------------------------------------------------------- /less/forum.less: -------------------------------------------------------------------------------- 1 | @import url('./forum/oauth'); 2 | 3 | .AuthorizedRecords { 4 | list-style: none; 5 | padding-inline-start: 0; 6 | 7 | .AuthorizedRecord-content { 8 | display: inline-flex; 9 | justify-content: space-between; 10 | width: 100%; 11 | 12 | .AuthorizedRecord-left { 13 | display: inline-flex; 14 | 15 | h3 { 16 | line-height: 1; 17 | margin-block-start: 0; 18 | margin-block-end: 0.5em; 19 | } 20 | 21 | .AuthorizedRecord-icon { 22 | display: inline-block; 23 | width: 40px; 24 | height: 40px; 25 | margin-right: 10px; 26 | overflow: hidden; 27 | background-color: #f0f0f0; 28 | 29 | img { 30 | width: 100%; 31 | height: 100%; 32 | } 33 | } 34 | } 35 | 36 | .AuthorizedRecord-right { 37 | display: grid; 38 | 39 | time { 40 | font-size: 0.8em; 41 | color: #666; 42 | float: right; 43 | } 44 | 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /migrations/2023_09_28_create_oauth_scopes_table.php: -------------------------------------------------------------------------------- 1 | function (Builder $schema) { 16 | if ($schema->hasTable('oauth_scopes')) { 17 | return; 18 | } 19 | $schema->create('oauth_scopes', function (Blueprint $table) { 20 | $table->increments('id'); 21 | $table->string('scope', 80); 22 | $table->string('resource_path', 500); 23 | $table->string('method', 20); 24 | $table->boolean('is_default')->nullable(); 25 | }); 26 | }, 27 | 'down' => function (Builder $schema) { 28 | $schema->dropIfExists('oauth_scopes'); 29 | }, 30 | ]; 31 | -------------------------------------------------------------------------------- /src/Api/Controller/DeleteAllRecordController.php: -------------------------------------------------------------------------------- 1 | settings = $settings; 21 | } 22 | protected function delete(ServerRequestInterface $request) 23 | { 24 | $actor = RequestUtil::getActor($request); 25 | $actor->assertAdmin(); 26 | 27 | $records = Record::where('user_id', '!=', 0); 28 | $records->delete(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Api/Serializer/ScopeUserSerializer.php: -------------------------------------------------------------------------------- 1 | $model->scope, 25 | "is_default" => $model->is_default, 26 | "scope_name" => $model->scope_name, 27 | "scope_icon" => $model->scope_icon, 28 | "scope_desc" => $model->scope_desc, 29 | ]; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Api/Controller/ListRecordController.php: -------------------------------------------------------------------------------- 1 | getQueryParams(), 'page', 0); 19 | 20 | $actor = RequestUtil::getActor($request); 21 | $actor->assertRegistered(); 22 | 23 | $pageSize = 10; 24 | $skip = $page * $pageSize; 25 | $records = Record::where('user_id', $actor->id) 26 | ->orderBy('authorized_at', 'desc') 27 | ->skip($skip) 28 | ->take($pageSize) 29 | ->get(); 30 | 31 | return $records; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /migrations/2023_10_02_add_default_record_to_oauth_scopes_table.php: -------------------------------------------------------------------------------- 1 | function (Builder $schema) { 16 | if (!$schema->hasTable('oauth_scopes')) { 17 | return; 18 | } 19 | $schema->getConnection()->table('oauth_scopes')->insert([ 20 | 'scope' => 'user.read', 21 | 'resource_path' => '/api/user', 22 | 'method' => 'GET', 23 | 'is_default' => 1, 24 | 'scope_name' => '获取用户信息', 25 | 'scope_icon' => 'fas fa-user', 26 | 'scope_desc' => '访问该用户({user})的个人信息等', 27 | ]); 28 | }, 29 | 'down' => function (Builder $schema) { 30 | 31 | }, 32 | ]; 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OAuth Center 2 | 3 | ![License](https://img.shields.io/badge/license-MIT-blue.svg) [![Latest Stable Version](https://img.shields.io/packagist/v/foskym/flarum-oauth-center.svg)](https://packagist.org/packages/foskym/flarum-oauth-center) [![Total Downloads](https://img.shields.io/packagist/dt/foskym/flarum-oauth-center.svg)](https://packagist.org/packages/foskym/flarum-oauth-center) 4 | 5 | A [Flarum](http://flarum.org) extension. Allow user to authorize the third clients 6 | 7 | ## Installation 8 | 9 | Install with composer: 10 | 11 | ```sh 12 | composer require foskym/flarum-oauth-center:"*" 13 | ``` 14 | 15 | ## Updating 16 | 17 | ```sh 18 | composer update foskym/flarum-oauth-center:"*" 19 | php flarum migrate 20 | php flarum cache:clear 21 | ``` 22 | 23 | ## Usage 24 | 25 | - [中文文档](/docs/zh.md) 26 | - [English Docs](/docs/en.md) 27 | 28 | ## Links 29 | 30 | - [Packagist](https://packagist.org/packages/foskym/flarum-oauth-center) 31 | - [GitHub](https://github.com/foskym/flarum-oauth-center) 32 | - [Discuss](https://discuss.flarum.org/d/33413-oauth-center) 33 | -------------------------------------------------------------------------------- /src/Api/Controller/ShowClientController.php: -------------------------------------------------------------------------------- 1 | getQueryParams(), 'client_id'); 19 | 20 | $actor = RequestUtil::getActor($request); 21 | $actor->assertRegistered(); 22 | 23 | if (!$actor->hasPermission('foskym-oauth-center.use-oauth')) { 24 | return []; 25 | } 26 | 27 | $client = Client::where('client_id', $client_id)->get()->first(); 28 | 29 | return $client; 30 | 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 FoskyM 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /src/Api/Controller/DeleteExpiredTokenController.php: -------------------------------------------------------------------------------- 1 | assertAdmin(); 21 | 22 | AccessToken::where('expires', '<', date('Y-m-d H:i:s'))->delete(); 23 | 24 | RefreshToken::where('expires', '<', date('Y-m-d H:i:s'))->delete(); 25 | 26 | AuthorizationCode::where('expires', '<', date('Y-m-d H:i:s'))->delete(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Api/Serializer/RecordSerializer.php: -------------------------------------------------------------------------------- 1 | client->makeHidden(['client_id', 'client_secret', 'redirect_uri', 'grant_types', 'scope', 'user_id']); 23 | return [ 24 | "id" => $model->id, 25 | "client" => $model->client, 26 | "user_id" => $model->user_id, 27 | "authorized_at" => $model->authorized_at 28 | ]; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /migrations/2023_09_28_create_oauth_access_tokens_table.php: -------------------------------------------------------------------------------- 1 | function (Builder $schema) { 16 | if ($schema->hasTable('oauth_access_tokens')) { 17 | return; 18 | } 19 | $schema->create('oauth_access_tokens', function (Blueprint $table) { 20 | $table->increments('id'); 21 | $table->string('access_token', 40); 22 | $table->string('client_id', 80); 23 | $table->string('user_id', 80)->nullable(); 24 | $table->timestamp('expires'); 25 | $table->string('scope', 4000)->nullable(); 26 | }); 27 | }, 28 | 'down' => function (Builder $schema) { 29 | $schema->dropIfExists('oauth_access_tokens'); 30 | }, 31 | ]; 32 | -------------------------------------------------------------------------------- /migrations/2023_09_28_create_oauth_refresh_tokens_table.php: -------------------------------------------------------------------------------- 1 | function (Builder $schema) { 16 | if ($schema->hasTable('oauth_refresh_tokens')) { 17 | return; 18 | } 19 | $schema->create('oauth_refresh_tokens', function (Blueprint $table) { 20 | $table->increments('id'); 21 | $table->string('refresh_token', 40); 22 | $table->string('client_id', 80); 23 | $table->string('user_id', 80)->nullable(); 24 | $table->timestamp('expires'); 25 | $table->string('scope', 4000)->nullable(); 26 | }); 27 | }, 28 | 'down' => function (Builder $schema) { 29 | $schema->dropIfExists('oauth_refresh_tokens'); 30 | }, 31 | ]; 32 | -------------------------------------------------------------------------------- /src/Api/Serializer/AccessTokenSerializer.php: -------------------------------------------------------------------------------- 1 | $model->id, 26 | "access_token" => $model->access_token, 27 | "client_id" => $model->client_id, 28 | "user_id" => $model->user_id, 29 | "expires" => $model->expires, 30 | "scope" => $model->scope 31 | ]; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Api/Controller/ListScopeController.php: -------------------------------------------------------------------------------- 1 | assertAdmin(); 22 | } catch (\Exception $e) { 23 | $actor->assertRegistered(); 24 | if (!$actor->hasPermission('foskym-oauth-center.use-oauth')) { 25 | return []; 26 | } 27 | $this->serializer = ScopeUserSerializer::class; 28 | } 29 | 30 | return Scope::all(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /migrations/2023_09_28_create_oauth_clients_table.php: -------------------------------------------------------------------------------- 1 | function (Builder $schema) { 16 | if ($schema->hasTable('oauth_clients')) { 17 | return; 18 | } 19 | $schema->create('oauth_clients', function (Blueprint $table) { 20 | $table->increments('id'); 21 | $table->string('client_id', 80); 22 | $table->string('client_secret', 80)->nullable(); 23 | $table->string('redirect_uri', 2000)->nullable(); 24 | $table->string('grant_types', 80)->nullable(); 25 | $table->string('scope', 4000)->nullable(); 26 | $table->string('user_id', 80)->nullable(); 27 | }); 28 | }, 29 | 'down' => function (Builder $schema) { 30 | $schema->dropIfExists('oauth_clients'); 31 | }, 32 | ]; 33 | -------------------------------------------------------------------------------- /js/src/forum/components/ScopeComponent.js: -------------------------------------------------------------------------------- 1 | import app from 'flarum/forum/app'; 2 | import Component from 'flarum/common/Component'; 3 | import Tooltip from 'flarum/common/components/Tooltip'; 4 | 5 | export default class ScopeComponent extends Component { 6 | view() { 7 | const { scope, client } = this.attrs; 8 | return ( 9 |
10 |
11 | { 12 | (scope.scope_icon().indexOf('fa-') > -1) ? 13 | : 15 | 16 | } 17 |
18 |
19 |
20 | {scope.scope_name()} 21 |
22 | 23 | { 24 | scope.scope_desc() 25 | .replace('{client_name}', client.client_name()) 26 | .replace('{user}', app.session.user.attribute('displayName')) 27 | } 28 | 29 |
30 |
31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Api/Controller/CreateClientController.php: -------------------------------------------------------------------------------- 1 | assertAdmin(); 20 | 21 | $attributes = Arr::get($request->getParsedBody(), 'data.attributes'); 22 | 23 | if ($client = Client::where('client_id', Arr::get($attributes, 'client_id'))->first()) { 24 | return $client; 25 | } 26 | 27 | return Client::create([ 28 | 'client_id' => Arr::get($attributes, 'client_id'), 29 | 'client_secret' => Arr::get($attributes, 'client_secret'), 30 | 'user_id' => $actor->id, 31 | ]); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Api/Serializer/ScopeSerializer.php: -------------------------------------------------------------------------------- 1 | $model->id, 25 | "scope" => $model->scope, 26 | "resource_path" => $model->resource_path, 27 | "method" => $model->method, 28 | "visible_fields" => $model->visible_fields, 29 | "is_default" => $model->is_default, 30 | "scope_name" => $model->scope_name, 31 | "scope_icon" => $model->scope_icon, 32 | "scope_desc" => $model->scope_desc, 33 | ]; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Api/Serializer/ClientPublicSerializer.php: -------------------------------------------------------------------------------- 1 | $model->id, 25 | "client_id" => $model->client_id, 26 | "redirect_uri" => $model->redirect_uri, 27 | "grant_types" => $model->grant_types, 28 | "scope" => $model->scope, 29 | "client_name" => $model->client_name, 30 | "client_icon" => $model->client_icon, 31 | "client_desc" => $model->client_desc, 32 | "client_home" => $model->client_home 33 | ]; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /js/src/forum/index.js: -------------------------------------------------------------------------------- 1 | import app from 'flarum/forum/app'; 2 | import {extend} from 'flarum/common/extend'; 3 | import UserPage from 'flarum/forum/components/UserPage'; 4 | import LinkButton from 'flarum/common/components/LinkButton'; 5 | import AuthorizePage from "./components/oauth/AuthorizePage"; 6 | import AuthorizedPage from "./components/user/AuthorizedPage"; 7 | app.initializers.add('foskym/flarum-oauth-center', () => { 8 | app.routes['oauth.authorize'] = { 9 | path: '/oauth/authorize', 10 | component: AuthorizePage 11 | }; 12 | 13 | app.routes['user.authorized'] = { 14 | path: '/u/:username/authorized', 15 | component: AuthorizedPage 16 | }; 17 | extend(UserPage.prototype, 'navItems', function (items) { 18 | if (app.session.user && app.session.user.id() === this.user.id()) { 19 | items.add( 20 | 'authorized', 21 | LinkButton.component( 22 | { 23 | href: app.route('user.authorized', { username: this.user.username() }), 24 | icon: 'fas fa-user-friends', 25 | }, 26 | [ 27 | app.translator.trans('foskym-oauth-center.forum.page.label.authorized'), 28 | ] 29 | ), 30 | -110 31 | ); 32 | } 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /migrations/2023_09_28_create_oauth_authorization_codes_table.php: -------------------------------------------------------------------------------- 1 | function (Builder $schema) { 16 | if ($schema->hasTable('oauth_authorization_codes')) { 17 | return; 18 | } 19 | $schema->create('oauth_authorization_codes', function (Blueprint $table) { 20 | $table->increments('id'); 21 | $table->string('authorization_code', 40); 22 | $table->string('client_id', 80); 23 | $table->string('user_id', 80)->nullable(); 24 | $table->string('redirect_uri', 2000)->nullable(); 25 | $table->timestamp('expires'); 26 | $table->string('scope', 4000)->nullable(); 27 | $table->string('id_token', 1000)->nullable(); 28 | }); 29 | }, 30 | 'down' => function (Builder $schema) { 31 | $schema->dropIfExists('oauth_authorization_codes'); 32 | }, 33 | ]; 34 | -------------------------------------------------------------------------------- /src/Api/Controller/DeleteUserRecordController.php: -------------------------------------------------------------------------------- 1 | settings = $settings; 21 | } 22 | protected function delete(ServerRequestInterface $request) 23 | { 24 | $actor = RequestUtil::getActor($request); 25 | $actor->assertRegistered(); 26 | 27 | $allow_delete_records = (bool) $this->settings->get('foskym-oauth-center.allow_delete_records'); 28 | 29 | if ($allow_delete_records) { 30 | $user_id = $actor->id; 31 | 32 | $record = Record::where('user_id', $user_id); 33 | $record->delete(); 34 | } 35 | 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Api/Controller/DeleteRecordController.php: -------------------------------------------------------------------------------- 1 | settings = $settings; 21 | } 22 | protected function delete(ServerRequestInterface $request) 23 | { 24 | $actor = RequestUtil::getActor($request); 25 | $actor->assertRegistered(); 26 | 27 | $allow_delete_records = (bool) $this->settings->get('foskym-oauth-center.allow_delete_records'); 28 | 29 | if ($allow_delete_records) { 30 | $id = Arr::get($request->getQueryParams(), 'id'); 31 | 32 | $record = Record::find($id); 33 | $record->delete(); 34 | } 35 | 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@foskym/flarum-oauth-center", 3 | "private": true, 4 | "version": "0.0.0", 5 | "devDependencies": { 6 | "flarum-webpack-config": "^2.0.0", 7 | "webpack": "^5.65.0", 8 | "webpack-cli": "^4.9.1", 9 | "prettier": "^2.5.1", 10 | "@flarum/prettier-config": "^1.0.0", 11 | "flarum-tsconfig": "^1.0.2", 12 | "typescript": "^4.5.4", 13 | "typescript-coverage-report": "^0.6.1" 14 | }, 15 | "scripts": { 16 | "dev": "webpack --mode development --watch", 17 | "build": "webpack --mode production", 18 | "analyze": "cross-env ANALYZER=true npm run build", 19 | "format": "prettier --write src", 20 | "format-check": "prettier --check src", 21 | "clean-typings": "npx rimraf dist-typings && mkdir dist-typings", 22 | "build-typings": "npm run clean-typings && ([ -e src/@types ] && cp -r src/@types dist-typings/@types || true) && tsc && npm run post-build-typings", 23 | "post-build-typings": "find dist-typings -type f -name '*.d.ts' -print0 | xargs -0 sed -i 's,../src/@types,@types,g'", 24 | "check-typings": "tsc --noEmit --emitDeclarationOnly false", 25 | "check-typings-coverage": "typescript-coverage-report" 26 | }, 27 | "prettier": "@flarum/prettier-config" 28 | } 29 | -------------------------------------------------------------------------------- /src/Middlewares/UnsetCsrfMiddleware.php: -------------------------------------------------------------------------------- 1 | getUri()->getPath(); 30 | if (in_array($path, $uri)) { 31 | $request = $request->withAttribute('bypassCsrfToken', true); 32 | } 33 | 34 | return $handler->handle($request); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Api/Serializer/ClientSerializer.php: -------------------------------------------------------------------------------- 1 | $model->id, 25 | "client_id" => $model->client_id, 26 | "client_secret" => $model->client_secret, 27 | "redirect_uri" => $model->redirect_uri, 28 | "grant_types" => $model->grant_types, 29 | "scope" => $model->scope, 30 | "user_id" => $model->user_id, 31 | "client_name" => $model->client_name, 32 | "client_icon" => $model->client_icon, 33 | "client_desc" => $model->client_desc, 34 | "client_home" => $model->client_home 35 | ]; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Controllers/TokenController.php: -------------------------------------------------------------------------------- 1 | settings = $settings; 29 | } 30 | 31 | public function handle(ServerRequestInterface $request): ResponseInterface 32 | { 33 | $oauth = new OAuth($this->settings); 34 | $server = $oauth->server(); 35 | 36 | $body = $server->handleTokenRequest($oauth->request()::createFromGlobals()) 37 | ->getResponseBody(); 38 | return new JsonResponse(json_decode($body, true)); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Api/Controller/UpdateScopeController.php: -------------------------------------------------------------------------------- 1 | assertAdmin(); 20 | 21 | $id = Arr::get($request->getQueryParams(), 'id'); 22 | $scope = Scope::find($id); 23 | 24 | $attributes = Arr::get($request->getParsedBody(), 'data.attributes', []); 25 | 26 | collect(['scope', 'resource_path', 'method', 'visible_fields', 'is_default', 'scope_name', 'scope_icon', 'scope_desc']) 27 | ->each(function (string $attribute) use ($scope, $attributes) { 28 | if (($val = Arr::get($attributes, $attribute)) !== null) { 29 | $scope->$attribute = $val; 30 | } 31 | }); 32 | 33 | $scope->save(); 34 | 35 | return $scope; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Middlewares/UserCredentialsMiddleware.php: -------------------------------------------------------------------------------- 1 | getUri()->getPath(); 23 | if (in_array($path, $uri) && Arr::get($request->getParsedBody(), 'grant_type', '') === 'password') { 24 | if ($user = User::where('username', Arr::get($request->getParsedBody(), 'username', ''))->first()) { 25 | if (!$user->hasPermission('foskym-oauth-center.use-oauth')) { 26 | return new JsonResponse([ 'error' => 'no_permission', 'error_description' => 'Don\'t have the permissions of oauth' ]); 27 | } 28 | } 29 | } 30 | 31 | return $handler->handle($request); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Api/Controller/UpdateClientController.php: -------------------------------------------------------------------------------- 1 | assertAdmin(); 20 | 21 | $id = Arr::get($request->getQueryParams(), 'id'); 22 | $client = Client::find($id); 23 | 24 | $attributes = Arr::get($request->getParsedBody(), 'data.attributes', []); 25 | 26 | collect(['client_id', 'client_secret', 'redirect_uri', 'grant_types', 'scope', 'client_name', 'client_desc', 'client_icon', 'client_home']) 27 | ->each(function (string $attribute) use ($client, $attributes) { 28 | if (($val = Arr::get($attributes, $attribute)) !== null) { 29 | if (($attribute == 'grant_types' || $attribute == 'scope') && $val === '') 30 | $client->$attribute = null; 31 | else 32 | $client->$attribute = $val; 33 | } 34 | }); 35 | 36 | $client->save(); 37 | 38 | return $client; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /js/src/admin/pages/TokensPage.js: -------------------------------------------------------------------------------- 1 | import app from 'flarum/admin/app'; 2 | import Page from 'flarum/common/components/Page'; 3 | import Button from 'flarum/common/components/Button'; 4 | import Alert from 'flarum/common/components/Alert'; 5 | import AddTokenModal from '../components/AddTokenModal'; 6 | 7 | export default class TokensPage extends Page { 8 | translationPrefix = 'foskym-oauth-center.admin.tokens.'; 9 | scopes = []; 10 | 11 | oninit(vnode) { 12 | super.oninit(vnode); 13 | 14 | this.fields = []; 15 | 16 | // app.store.find('oauth-tokens').then(r => { 17 | // this.scopes = r; 18 | // m.redraw(); 19 | // }); 20 | } 21 | 22 | view() { 23 | return ( 24 |
25 | {[ 26 | Button.component({ 27 | type: 'button', 28 | className: 'Button', 29 | onclick: () => { 30 | this.showAddModal(); 31 | } 32 | }, app.translator.trans('foskym-oauth-center.admin.tokens.add_token')), 33 | Button.component({ 34 | type: 'button', 35 | className: 'Button', 36 | onclick: () => { 37 | this.deleteExpiredTokens(); 38 | } 39 | }, app.translator.trans('foskym-oauth-center.admin.tokens.delete_token')) 40 | ]} 41 |
42 | ); 43 | } 44 | 45 | deleteExpiredTokens() { 46 | app.request({ 47 | method: 'DELETE', 48 | url: '/api/oauth-tokens/expired' 49 | }).then(() => { 50 | app.alerts.show( 51 | Alert, 52 | {type: 'success'}, 53 | 'success!' 54 | ) 55 | }); 56 | } 57 | 58 | showAddModal() { 59 | app.modal.show(AddTokenModal, {}); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Controllers/ApiUserController.php: -------------------------------------------------------------------------------- 1 | settings = $settings; 29 | } 30 | 31 | public function handle(ServerRequestInterface $request): ResponseInterface 32 | { 33 | $actor = RequestUtil::getActor($request); 34 | $actor = $actor->toArray(); 35 | $data = [ 36 | 'id' => $actor['id'], 37 | 'username' => $actor['username'], 38 | 'nickname' => $actor['nickname'], 39 | 'avatar_url' => $actor['avatar_url'], 40 | 'email' => $actor['email'], 41 | 'is_email_confirmed' => $actor['is_email_confirmed'], 42 | 'joined_at' => $actor['joined_at'], 43 | 'last_seen_at' => $actor['last_seen_at'], 44 | 'discussion_count' => $actor['discussion_count'], 45 | 'comment_count' => $actor['comment_count'], 46 | ]; 47 | return new JsonResponse($data); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /locale/zh-Hans.yml: -------------------------------------------------------------------------------- 1 | foskym-oauth-center: 2 | admin: 3 | permission: 4 | use_oauth: 使用 OAuth 授权 5 | page: 6 | index: 首页 7 | clients: 应用管理 8 | scopes: 权限管理 9 | tokens: 令牌管理 10 | settings: 11 | display_mode: 显示模式 12 | access_lifetime: 访问令牌有效期(秒) 13 | allow_implicit: 允许隐式授权(response_type=token) 14 | enforce_state: 强制状态验证(state 参数) 15 | require_exact_redirect_uri: 需要精确的重定向 URI 16 | authorization_method_fetch: 授权使用 Fetch 方式请求(非直接转向页面) 17 | allow_delete_records: 允许删除授权记录 18 | clients: 19 | client_id: 应用 ID 20 | client_secret: 应用密钥 21 | redirect_uri: 回调地址 22 | grant_types: 授权类型 23 | scope: 权限 24 | client_name: 名称 25 | client_desc: 描述 26 | client_icon: 图标 27 | client_home: 主页 28 | add_button: 添加应用 29 | edit_client: 编辑应用 30 | scopes: 31 | scope: 权限标识 32 | resource_path: 资源路径 33 | method: 请求方法 34 | is_default: 默认 35 | scope_name: 名称 36 | scope_icon: 图标 37 | scope_desc: 描述 38 | visible_fields: 可见字段 39 | add_button: 添加权限 40 | edit_scope: 编辑权限 41 | tokens: 42 | add_token: 添加令牌 43 | delete_token: 删除过期令牌 44 | action: 45 | delete_all_records_button: 删除所有记录 46 | delete_all_records_confirm: 确认删除所有记录 47 | delete_all_records_success: 删除所有记录成功 48 | 49 | forum: 50 | page: 51 | title: 52 | authorize: 授权 53 | label: 54 | authorized: 授权记录 55 | authorize: 56 | require_these_scopes: 该应用需要以下权限 57 | access: 授权访问 58 | agree: 授权 59 | deny: 拒绝 60 | authorized: 61 | no_records: 暂无记录 62 | no_more_records: 没有更多记录了 63 | load_more: 加载更多 64 | delete_button: 删除此记录 65 | delete_all_button: 删除所有记录 66 | delete_all_confirm: 确认删除所有记录 67 | -------------------------------------------------------------------------------- /src/Api/Controller/CreateTokenController.php: -------------------------------------------------------------------------------- 1 | assertAdmin(); 20 | 21 | $attributes = Arr::get($request->getParsedBody(), 'data.attributes'); 22 | 23 | $access_token = Arr::get($attributes, 'access_token'); 24 | $client_id = Arr::get($attributes, 'client_id'); 25 | $user_id = Arr::get($attributes, 'user_id'); 26 | $scope = Arr::get($attributes, 'scope'); 27 | $expires = date('Y-m-d H:i:s', strtotime(Arr::get($attributes, 'expires')) ?: time() + 3600 * 24); 28 | 29 | if (!$access_token || !$client_id || !$user_id || !$scope) { 30 | throw new \Exception('Invalid token attributes'); 31 | } 32 | 33 | if (AccessToken::where('access_token', Arr::get($attributes, 'access_token'))->first()) { 34 | return AccessToken::where('access_token', Arr::get($attributes, 'access_token'))->first(); 35 | } 36 | 37 | return AccessToken::create([ 38 | 'access_token' => Arr::get($attributes, 'access_token'), 39 | 'client_id' => Arr::get($attributes, 'client_id'), 40 | 'user_id' => Arr::get($attributes, 'user_id'), 41 | 'expires' => $expires, 42 | 'scope' => Arr::get($attributes, 'scope'), 43 | ]); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /js/src/admin/components/SettingsPage.js: -------------------------------------------------------------------------------- 1 | import app from 'flarum/admin/app'; 2 | import ExtensionPage from 'flarum/admin/components/ExtensionPage'; 3 | import Button from 'flarum/common/components/Button'; 4 | import IndexPage from "../pages/IndexPage"; 5 | import ClientsPage from "../pages/ClientsPage"; 6 | import ScopesPage from "../pages/ScopesPage"; 7 | import TokensPage from "../pages/TokensPage"; 8 | 9 | export default class SettingsPage extends ExtensionPage { 10 | translationPrefix = 'foskym-oauth-center.admin.page.'; 11 | pages = { 12 | index: IndexPage, 13 | clients: ClientsPage, 14 | scopes: ScopesPage, 15 | tokens: TokensPage 16 | }; 17 | icons = { 18 | index: 'home', 19 | clients: 'network-wired', 20 | scopes: 'user-lock', 21 | tokens: 'key', 22 | }; 23 | 24 | content() { 25 | const page = m.route.param().page || 'index'; 26 | 27 | return ( 28 |
29 |
30 |
31 | {this.menuButtons(page)} 32 |
33 |
34 | 35 |
36 | {this.pageContent(page)} 37 |
38 |
39 | ); 40 | } 41 | 42 | // Return button menus 43 | menuButtons(page) { 44 | return Object.keys(this.pages).map(key => 45 | Button.component({ 46 | className: `Button ${page === key ? 'item-selected' : ''}`, 47 | onclick: () => m.route.set( 48 | app.route('extension', { 49 | id: 'foskym-oauth-center', 50 | page: key 51 | }) 52 | ), 53 | icon: this.iconForPage(key), 54 | }, app.translator.trans(this.translationPrefix + key)) 55 | ); 56 | } 57 | 58 | iconForPage(page) { 59 | return `fas fa-${this.icons[page]}` || ''; 60 | } 61 | 62 | pageContent(page) { 63 | const PageComponent = this.pages[page]; 64 | return PageComponent ? : null; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /js/src/admin/components/AddTokenModal.js: -------------------------------------------------------------------------------- 1 | import app from 'flarum/admin/app'; 2 | import Modal from 'flarum/common/components/Modal'; 3 | import Button from 'flarum/common/components/Button'; 4 | import Stream from 'flarum/common/utils/Stream'; 5 | export default class AddTokenModal extends Modal { 6 | oninit(vnode) { 7 | super.oninit(vnode); 8 | 9 | this.fields = [ 10 | 'access_token', 11 | 'client_id', 12 | 'user_id', 13 | 'expires', 14 | 'scope' 15 | ]; 16 | 17 | this.values = this.fields.reduce((values, key) => { 18 | values[key] = Stream(''); 19 | return values; 20 | }, {}); 21 | } 22 | 23 | className() { 24 | return 'AddTokenModal'; 25 | } 26 | 27 | title() { 28 | return app.translator.trans('foskym-oauth-center.admin.tokens.add_token'); 29 | } 30 | 31 | content() { 32 | return ( 33 |
34 |
35 | {this.fields.map(key => 36 |
37 | 38 | 39 |
40 | )} 41 |
42 | {Button.component({ 43 | type: 'submit', 44 | className: 'Button Button--primary Button--block EditClientModal-save', 45 | loading: this.loading, 46 | }, app.translator.trans('core.admin.settings.submit_button'))} 47 |
48 |
49 |
50 | ); 51 | } 52 | 53 | onsubmit(e) { 54 | e.preventDefault(); 55 | this.loading = true; 56 | 57 | const token = app.store.createRecord('oauth-tokens'); 58 | 59 | const tokenData = this.fields.reduce((data, key) => { 60 | data[key] = this.values[key](); 61 | return data; 62 | }, {}); 63 | 64 | token.save(tokenData).then(() => { 65 | this.loading = false; 66 | m.redraw(); 67 | this.hide(); 68 | }); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/OAuth.php: -------------------------------------------------------------------------------- 1 | settings = $settings; 30 | } 31 | public function response(): Response 32 | { 33 | return new Response; 34 | } 35 | public function request(): Request 36 | { 37 | return new Request; 38 | } 39 | 40 | public function storage(): Storage 41 | { 42 | return new Storage; 43 | } 44 | public function server(): Server 45 | { 46 | $storage = new Storage; 47 | $server = new Server($storage, array( 48 | 'allow_implicit' => $this->settings->get('foskym-oauth-center.allow_implicit') == "1", 49 | 'enforce_state' => $this->settings->get('foskym-oauth-center.enforce_state') == "1", 50 | 'require_exact_redirect_uri' => $this->settings->get('foskym-oauth-center.require_exact_redirect_uri') == "1", 51 | 'access_lifetime' => $this->settings->get('foskym-oauth-center.access_lifetime') == "" ? 3600 : $this->settings->get('foskym-oauth-center.access_lifetime'), 52 | )); 53 | $server->addGrantType(new AuthorizationCode($storage)); 54 | $server->addGrantType(new ClientCredentials($storage)); 55 | $server->addGrantType(new UserCredentials($storage)); 56 | $server->addGrantType(new RefreshToken($storage)); 57 | return $server; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /less/admin.less: -------------------------------------------------------------------------------- 1 | .OAuthCenter { 2 | padding-top: 0 !important; 3 | margin-top: 0 !important; 4 | 5 | .hidden { 6 | display: none; 7 | } 8 | 9 | .pull-right { 10 | float: right; 11 | } 12 | 13 | .oauth-menu { 14 | background: @control-bg; 15 | margin-bottom: 15px; 16 | 17 | 18 | .Button { 19 | padding: 12px 13px; 20 | border-radius: 0; 21 | 22 | &.item-selected { 23 | background: darken(@control-bg, 10%); 24 | } 25 | } 26 | } 27 | 28 | .OAuthCenterPage-container { 29 | max-width: 100% !important; 30 | } 31 | 32 | .OAuthCenter-clientsPage, .OAuthCenter-scopesPage { 33 | table { 34 | width: 100%; 35 | 36 | td, th { 37 | padding: 3px 5px; 38 | } 39 | 40 | th { 41 | text-align: left; 42 | } 43 | } 44 | 45 | .Checkbox { 46 | padding: 0 10px; 47 | } 48 | 49 | .FormControl { 50 | background: @body-bg; 51 | border-color: @control-bg; 52 | 53 | // We set the same as Flarum default, but with more specificity 54 | &:focus, 55 | &.focus { 56 | border-color: @primary-color; 57 | } 58 | } 59 | } 60 | } 61 | .OAuthCenter-Checkbox { 62 | .Checkbox-display { 63 | font-size: 22px; 64 | } 65 | } 66 | .OAuthCenter-Columns { 67 | display: flex; 68 | flex-wrap: wrap; 69 | justify-content: space-between; 70 | 71 | .OAuthCenter-Column { 72 | flex: 1; 73 | margin-right: 25px; 74 | margin-bottom: 20px; 75 | 76 | &:last-child { 77 | margin-right: 0; 78 | } 79 | } 80 | } 81 | @media (max-width: 992px) { 82 | .OAuthCenter-Columns { 83 | display: block; 84 | .OAuthCenter-Column { 85 | margin-right: 0; 86 | } 87 | } 88 | } 89 | .OAuthCenter-FormGroup-Column { 90 | width: 48%; 91 | display: inline-block; 92 | vertical-align: text-top; 93 | &:last-child { 94 | text-align: center; 95 | } 96 | } 97 | 98 | @media (min-width: 992px) { 99 | .OAuthCenter { 100 | .OAuthCenterPage-container { 101 | max-width: 600px; 102 | margin: 0; 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /locale/en.yml: -------------------------------------------------------------------------------- 1 | foskym-oauth-center: 2 | admin: 3 | permission: 4 | use_oauth: Use OAuth 5 | page: 6 | index: home 7 | clients: Manage Clients 8 | scopes: Manage Scopes 9 | tokens: Manage Tokens 10 | settings: 11 | display_mode: Display Mode 12 | access_lifetime: Access Token Lifetime (seconds) 13 | allow_implicit: Allow Implicit Grant (response_type=token) 14 | enforce_state: Enforce State Validation (state parameter) 15 | require_exact_redirect_uri: Require Exact Redirect URI 16 | authorization_method_fetch: Authorization Method Fetch (Not directly redirecting to) 17 | allow_delete_records: Allow Delete Authorization Records 18 | clients: 19 | client_id: Client ID 20 | client_secret: Client Secret 21 | redirect_uri: Redirect URI 22 | grant_types: Grant Types 23 | scope: Scope 24 | client_name: Name 25 | client_desc: Description 26 | client_icon: Icon 27 | client_home: HomePage 28 | add_button: Add Client 29 | edit_client: Edit Client 30 | scopes: 31 | scope: Scope 32 | resource_path: Resource Path 33 | method: Method 34 | is_default: Default 35 | scope_name: Name 36 | scope_icon: Icon 37 | scope_desc: Description 38 | visible_fields: Visible Fields 39 | add_button: Add Scope 40 | edit_scope: Edit Scope 41 | tokens: 42 | add_token: Add Token 43 | delete_token: Delete Token 44 | action: 45 | delete_all_records_button: Delete all records 46 | delete_all_records_confirm: Confirm delete all records 47 | delete_all_records_success: Delete all records successfully 48 | 49 | forum: 50 | page: 51 | title: 52 | authorize: Authorize 53 | label: 54 | authorized: Authorized Logs 55 | authorize: 56 | require_these_scopes: Require these scopes 57 | access: Access to 58 | agree: Agree 59 | deny: Deny 60 | authorized: 61 | no_records: No records 62 | no_more_records: No more records 63 | load_more: Load More 64 | delete_button: Delete this log 65 | delete_all_button: Delete all logs 66 | delete_all_confirm: Confirm delete all logs 67 | -------------------------------------------------------------------------------- /src/Controllers/AuthorizeController.php: -------------------------------------------------------------------------------- 1 | settings = $settings; 31 | } 32 | 33 | public function handle(ServerRequestInterface $request): ResponseInterface 34 | { 35 | $actor = RequestUtil::getActor($request); 36 | $actor->assertRegistered(); 37 | 38 | if (!$actor->hasPermission('foskym-oauth-center.use-oauth')) { 39 | return new HtmlResponse('Don\'t have the permissions of oauth'); 40 | } 41 | 42 | $params = $request->getParsedBody(); 43 | 44 | $oauth = new OAuth($this->settings); 45 | $server = $oauth->server(); 46 | $request = $oauth->request()::createFromGlobals(); 47 | $response = $oauth->response(); 48 | 49 | if (!$server->validateAuthorizeRequest($request, $response)) { 50 | $response->send(); 51 | die; 52 | } 53 | 54 | $is_authorized = (bool) Arr::get($params, 'is_authorized', 0); 55 | if ($is_authorized) { 56 | Record::create([ 57 | 'client_id' => Arr::get($params, 'client_id'), 58 | 'user_id' => $actor->id, 59 | 'authorized_at' => date('Y-m-d H:i:s') 60 | ]); 61 | } 62 | $server->handleAuthorizeRequest($request, $response, $is_authorized, $actor->id); 63 | $response->send(); 64 | die; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Middlewares/ResourceScopeFieldsMiddleware.php: -------------------------------------------------------------------------------- 1 | settings = $settings; 28 | } 29 | public function process(Request $request, RequestHandlerInterface $handler): Response 30 | { 31 | $actor = RequestUtil::getActor($request); 32 | $response = $handler->handle($request); 33 | if ($scopes = $request->getAttribute('oauth.scopes')) { 34 | return $this->filterResponse($response, $scopes); 35 | } 36 | return $response; 37 | } 38 | 39 | private function filterResponse(Response $response, $scopes) 40 | { 41 | $data = []; 42 | $origin = json_decode($response->getBody(), true); 43 | 44 | $visible_fields = []; 45 | 46 | foreach ($scopes as $scope) { 47 | $visible_fields = array_merge($visible_fields, explode(',', $scope['visible_fields'])); 48 | } 49 | 50 | $visible_fields = array_unique(array_values(array_filter($visible_fields))); 51 | 52 | if (empty($visible_fields)) { 53 | return $response; 54 | } 55 | 56 | foreach ($visible_fields as $visible_field) { 57 | if (Arr::has($origin, $visible_field)) { 58 | Arr::set($data, $visible_field, Arr::get($origin, $visible_field)); 59 | } 60 | } 61 | 62 | return new JsonResponse($data, $response->getStatusCode()); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "foskym/flarum-oauth-center", 3 | "description": "Allow user to authorize the third clients", 4 | "keywords": [ 5 | "flarum", 6 | "user", 7 | "oauth", 8 | "center" 9 | ], 10 | "type": "flarum-extension", 11 | "license": "MIT", 12 | "require": { 13 | "flarum/core": "^1.8", 14 | "bshaffer/oauth2-server-php": "*" 15 | }, 16 | "authors": [ 17 | { 18 | "name": "FoskyM", 19 | "email": "i@fosky.top", 20 | "role": "Developer", 21 | "homepage": "https://fosky.top" 22 | } 23 | ], 24 | "autoload": { 25 | "psr-4": { 26 | "FoskyM\\OAuthCenter\\": "src/" 27 | } 28 | }, 29 | "extra": { 30 | "flarum-extension": { 31 | "title": "OAuth Center", 32 | "category": "", 33 | "icon": { 34 | "name": "fas fa-user-friends", 35 | "color": "#fff", 36 | "backgroundColor": "#c12c1f" 37 | } 38 | }, 39 | "flarum-cli": { 40 | "modules": { 41 | "admin": true, 42 | "forum": true, 43 | "js": true, 44 | "jsCommon": true, 45 | "css": true, 46 | "locale": true, 47 | "gitConf": true, 48 | "githubActions": true, 49 | "prettier": true, 50 | "typescript": true, 51 | "bundlewatch": false, 52 | "backendTesting": true, 53 | "editorConfig": true, 54 | "styleci": true 55 | } 56 | } 57 | }, 58 | "minimum-stability": "dev", 59 | "prefer-stable": true, 60 | "autoload-dev": { 61 | "psr-4": { 62 | "FoskyM\\OAuthCenter\\Tests\\": "tests/" 63 | } 64 | }, 65 | "scripts": { 66 | "test": [ 67 | "@test:unit", 68 | "@test:integration" 69 | ], 70 | "test:unit": "phpunit -c tests/phpunit.unit.xml", 71 | "test:integration": "phpunit -c tests/phpunit.integration.xml", 72 | "test:setup": "@php tests/integration/setup.php" 73 | }, 74 | "scripts-descriptions": { 75 | "test": "Runs all tests.", 76 | "test:unit": "Runs all unit tests.", 77 | "test:integration": "Runs all integration tests.", 78 | "test:setup": "Sets up a database for use with integration tests. Execute this only once." 79 | }, 80 | "require-dev": { 81 | "flarum/testing": "^1.0.0" 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Controllers/AuthorizeFetchController.php: -------------------------------------------------------------------------------- 1 | settings = $settings; 30 | } 31 | 32 | public function handle(ServerRequestInterface $request): ResponseInterface 33 | { 34 | $actor = RequestUtil::getActor($request); 35 | $actor->assertRegistered(); 36 | 37 | if (!$actor->hasPermission('foskym-oauth-center.use-oauth')) { 38 | return new JsonResponse([ 'error' => 'no_permission', 'error_description' => 'Don\'t have the permissions of oauth' ]); 39 | } 40 | 41 | $params = $request->getParsedBody(); 42 | 43 | $oauth = new OAuth($this->settings); 44 | $server = $oauth->server(); 45 | $request = $oauth->request()::createFromGlobals(); 46 | $response = $oauth->response(); 47 | 48 | if (!$server->validateAuthorizeRequest($request, $response)) { 49 | return new JsonResponse(json_decode($response->getResponseBody(), true)); 50 | } 51 | 52 | $is_authorized = Arr::get($params, 'is_authorized', 0); 53 | $server->handleAuthorizeRequest($request, $response, $is_authorized, $actor->id); 54 | if ($is_authorized) { 55 | Record::create([ 56 | 'client_id' => Arr::get($params, 'client_id'), 57 | 'user_id' => $actor->id, 58 | 'authorized_at' => date('Y-m-d H:i:s') 59 | ]); 60 | // $code = substr($response->getHttpHeader('Location'), strpos($response->getHttpHeader('Location'), 'code=') + 5, 40); 61 | return new JsonResponse([ 62 | 'location' => $response->getHttpHeader('Location') 63 | ]); 64 | } 65 | 66 | return new JsonResponse(json_decode($response->getResponseBody(), true)); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /js/src/admin/components/EditClientModal.js: -------------------------------------------------------------------------------- 1 | import app from 'flarum/admin/app'; 2 | import Modal from 'flarum/common/components/Modal'; 3 | import Button from 'flarum/common/components/Button'; 4 | import Stream from 'flarum/common/utils/Stream'; 5 | export default class EditClientModal extends Modal { 6 | oninit(vnode) { 7 | super.oninit(vnode); 8 | 9 | this.client = this.attrs.client; 10 | this.fields = [ 11 | 'client_id', 12 | 'client_secret', 13 | 'redirect_uri', 14 | 'grant_types', 15 | 'scope', 16 | 17 | 'client_name', 18 | 'client_desc', 19 | 'client_icon', 20 | 'client_home' 21 | ]; 22 | 23 | this.values = this.fields.reduce((values, key) => { 24 | values[key] = Stream(this.client[key]() || ''); 25 | return values; 26 | }, {}); 27 | } 28 | 29 | className() { 30 | return 'EditClientModal Modal--large'; 31 | } 32 | 33 | title() { 34 | return app.translator.trans('foskym-oauth-center.admin.clients.edit_client'); 35 | } 36 | 37 | content() { 38 | return ( 39 |
40 |
41 |
42 |
43 | {this.fields.slice(0, 5).map(key => 44 |
45 | 46 | 47 |
48 | )} 49 |
50 |
51 | {this.fields.slice(5, 9).map(key => 52 |
53 | 54 | 55 |
56 | )} 57 |
58 |
59 | 60 |
61 | {Button.component({ 62 | type: 'submit', 63 | className: 'Button Button--primary Button--block EditClientModal-save', 64 | loading: this.loading, 65 | }, app.translator.trans('core.admin.settings.submit_button'))} 66 |
67 |
68 |
69 | ); 70 | } 71 | 72 | onsubmit(e) { 73 | e.preventDefault(); 74 | 75 | this.loading = true; 76 | 77 | const clientData = this.fields.reduce((data, key) => { 78 | data[key] = this.values[key](); 79 | return data; 80 | }, {}); 81 | 82 | this.client.save(clientData).then(() => { 83 | this.loading = false; 84 | m.redraw(); 85 | this.hide(); 86 | }); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /js/src/admin/pages/ClientsPage.js: -------------------------------------------------------------------------------- 1 | import app from 'flarum/admin/app'; 2 | import Page from 'flarum/common/components/Page'; 3 | import Button from 'flarum/common/components/Button'; 4 | import EditClientModal from '../components/EditClientModal'; 5 | import {randomString} from "../../common/utils/randomString"; 6 | 7 | export default class ClientsPage extends Page { 8 | translationPrefix = 'foskym-oauth-center.admin.clients.'; 9 | clients = []; 10 | 11 | oninit(vnode) { 12 | super.oninit(vnode); 13 | 14 | app.store.find('oauth-clients').then(r => { 15 | this.clients = r; 16 | m.redraw(); 17 | }); 18 | } 19 | 20 | view() { 21 | return ( 22 |
23 | { 24 | m('.Form-group', [ 25 | m('table', [ 26 | m('thead', m('tr', [ 27 | ['client_id', 'client_name'].map(key => m('th', app.translator.trans(this.translationPrefix + key))), 28 | m('th'), 29 | ])), 30 | m('tbody', [ 31 | this.clients.map((client, index) => m('tr', [ 32 | ['client_id', 'client_name'].map(key => 33 | m('td', client[key]()) 34 | ), 35 | m('td', [ 36 | Button.component({ 37 | className: 'Button Button--icon', 38 | icon: 'fas fa-edit', 39 | onclick: () => this.showEditModal(client), 40 | }), 41 | Button.component({ 42 | className: 'Button Button--icon', 43 | icon: 'fas fa-times', 44 | onclick: () => { 45 | client.delete(); 46 | this.clients.splice(index, 1); 47 | }, 48 | }), 49 | ]), 50 | ])), 51 | m('tr', m('td', { 52 | colspan: 2, 53 | }, Button.component({ 54 | className: 'Button Button--block', 55 | onclick: () => { 56 | const client_id = randomString(32); 57 | const client_secret = randomString(32); 58 | app.store.createRecord('oauth-clients').save({ 59 | client_id: client_id, 60 | client_secret: client_secret, 61 | }).then(client => { 62 | this.clients.push(client); 63 | this.showEditModal(client); 64 | }); 65 | }, 66 | }, app.translator.trans(this.translationPrefix + 'add_button')))), 67 | ]), 68 | ]), 69 | ]) 70 | } 71 |
72 | ); 73 | } 74 | 75 | showEditModal(client) { 76 | app.modal.show(EditClientModal, { client: client }); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /js/src/admin/pages/ScopesPage.js: -------------------------------------------------------------------------------- 1 | import app from 'flarum/admin/app'; 2 | import Page from 'flarum/common/components/Page'; 3 | import Button from 'flarum/common/components/Button'; 4 | import {randomString} from "../../common/utils/randomString"; 5 | import EditScopeModal from "../components/EditScopeModal"; 6 | 7 | export default class ScopesPage extends Page { 8 | translationPrefix = 'foskym-oauth-center.admin.scopes.'; 9 | scopes = []; 10 | 11 | oninit(vnode) { 12 | super.oninit(vnode); 13 | 14 | this.fields = [ 15 | 'scope', 16 | 'resource_path', 17 | 'method', 18 | 'is_default', 19 | 'scope_name', 20 | 'scope_icon', 21 | 'scope_desc' 22 | ]; 23 | 24 | app.store.find('oauth-scopes').then(r => { 25 | this.scopes = r; 26 | m.redraw(); 27 | }); 28 | } 29 | 30 | view() { 31 | return ( 32 |
33 | { 34 | m('.Form-group', [ 35 | m('table', [ 36 | m('thead', m('tr', [ 37 | ['scope', 'scope_name'].map(key => m('th', app.translator.trans(this.translationPrefix + key))), 38 | m('th'), 39 | ])), 40 | m('tbody', [ 41 | this.scopes.map((scope, index) => m('tr', [ 42 | ['scope', 'scope_name'].map(key => 43 | m('td', scope[key]()) 44 | ), 45 | m('td', [ 46 | Button.component({ 47 | className: 'Button Button--icon', 48 | icon: 'fas fa-edit', 49 | onclick: () => this.showEditModal(scope), 50 | }), 51 | Button.component({ 52 | className: 'Button Button--icon', 53 | icon: 'fas fa-times', 54 | onclick: () => { 55 | scope.delete(); 56 | this.scopes.splice(index, 1); 57 | }, 58 | }), 59 | ]), 60 | ])), 61 | m('tr', m('td', { 62 | colspan: 2, 63 | }, Button.component({ 64 | className: 'Button Button--block', 65 | onclick: () => { 66 | app.store.createRecord('oauth-scopes') 67 | .save({ 68 | 'scope': 'Scope.' + randomString(8), 69 | 'resource_path': '/api/' + randomString(4), 70 | 'method': 'GET', 71 | }) 72 | .then(scope => { 73 | this.scopes.push(scope); 74 | this.showEditModal(scope); 75 | }); 76 | }, 77 | }, app.translator.trans(this.translationPrefix + 'add_button')))), 78 | ]), 79 | ]), 80 | ]) 81 | } 82 |
83 | ); 84 | } 85 | 86 | showEditModal(scope) { 87 | app.modal.show(EditScopeModal, {scope: scope}); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /docs/zh.md: -------------------------------------------------------------------------------- 1 | ### 配置项 2 | ![Snipaste_2023-10-02_06-15-33](https://github.com/FoskyM/flarum-oauth-center/assets/39661663/0e8352bf-0aeb-4605-bd84-aaedf8cae0e8) 3 | #### 配置项说明 4 | - `允许隐式授权` response_type=token 的方式,令牌直接通过 hash 返回给客户端,详细说明可百度 5 | - `强制状态验证` state 参数必须存在 6 | - `精确的重定向 URI` 传参时的重定向 URI 必须和创建应用时填写的一致 7 | - `令牌有效期` 令牌的有效期,单位为秒 8 | 9 | ### 创建应用 10 | ![Snipaste_2023-10-02_06-15-52](https://github.com/FoskyM/flarum-oauth-center/assets/39661663/e9879852-f4ec-4a04-9f81-58004692493c) 11 | #### 应用创建说明 12 | - `应用名称` 应用的名称 13 | - `应用描述` 应用的描述 14 | - `应用图标` 应用的图标,可选 15 | - `应用主页` 应用的主页,可选 16 | - `应用回调地址` 应用的回调地址,必填,多个地址使用空格分隔(不推荐单个应用使用多个地址) 17 | - `权限` 可选(不清楚的话不要填) 18 | - `授权类型` 可选(不清楚的话不要填) 19 | - `应用 ID` 和 `应用密钥` 用于客户端认证,添加应用时自动生成,不要泄露给其他人 20 | 21 | ### 设置资源控制器的权限 (user.read 项是默认生成的权限) 22 | ![Snipaste_2023-10-02_06-16-06](https://github.com/FoskyM/flarum-oauth-center/assets/39661663/31648ad2-4326-47e0-9d26-c0e3b2f30f8d) 23 | 大部分人只需要 `user.read` 权限即可,如果你需要更多的权限,可以在这里添加(或许你需要先了解一下 OAuth 中的 scope) 24 | #### 权限说明 25 | - `权限标识` 权限唯一标识符,用于区分,可参考 `Github` 的权限标识 26 | - `资源路径` 需要鉴权的资源路径 27 | - `请求方法` 资源路径鉴权的请求方法,一般为 `GET` 28 | - `默认` 勾选此项后哪怕传参时 scope 参数中无此权限标识,也会默认添加此权限 29 | - `名称` 权限的名称,用于显示 30 | - `描述` 权限的描述,用于显示,可使用 `{user}` `{client_name}` 变量指代用户和客户端名称 31 | - `图标` 支持 `FontAwesome` 图标和普通图片 32 | 33 | ### 本插件相关路径 34 | #### 授权 35 | `/oauth/authorize` 36 | 37 | | 参数 | 说明 | 必填 | 默认值 | 示例 | 38 | | --- | --- | --- | --- | --- | 39 | | client_id | 应用 ID | 是 | 无 | 123456 | 40 | | response_type | 授权类型 | 是 | 无 | code 或 token | 41 | | redirect_uri | 重定向 URI | 是 | 应用回调地址 | https://example.com/oauth/callback | 42 | | scope | 权限 | 否 | 无 | user.read | 43 | | state | 状态 | 否 | 无 | 123456 | 44 | 45 | 示例: 46 | ``` 47 | GET https://example.com/oauth/authorize?client_id=123456&response_type=code&redirect_uri=https://user.example.com/oauth/callback&scope=user.read&state=123456 48 | ``` 49 | 50 | #### 令牌 51 | `/oauth/token` 52 | 53 | | 参数 | 说明 | 必填 | 默认值 | 示例 | 54 | | --- | --- | --- | --- | --- | 55 | | client_id | 应用 ID | 是 | 无 | 123456 | 56 | | client_secret | 应用密钥 | 是 | 无 | 123456 | 57 | | grant_type | 授权类型 | 是 | 无 | authorization_code 或 refresh_token | 58 | | code | 授权码 | 授权类型为 authorization_code 时必填 | 无 | 123456 | 59 | | refresh_token | 刷新令牌 | 授权类型为 refresh_token 时必填 | 无 | 123456 | 60 | | redirect_uri | 重定向 URI | 授权类型为 authorization_code 时必填 | 应用回调地址 | https://example.com/oauth/callback | 61 | 62 | 示例: 63 | 64 | ``` 65 | POST https://example.com/oauth/token 66 | 67 | Payload: client_id=123456&client_secret=123456&grant_type=authorization_code&code=123456&redirect_uri=https://example.com/oauth/callback 68 | ``` 69 | 70 | #### 资源(用户) 71 | `/api/user` 72 | 73 | | 参数 | 说明 | 必填 | 默认值 | 示例 | 74 | | --- | --- | --- | --- | --- | 75 | | access_token | 访问令牌 | 是 | 无 | 123456 | 76 | 77 | 示例: 78 | 79 | ``` 80 | GET https://example.com/api/user?access_token=123456 81 | ``` 82 | 83 | ### 和常规的 OAuth 应用一样使用 84 | ![Snipaste_2023-10-02_06-16-31](https://github.com/FoskyM/flarum-oauth-center/assets/39661663/1632672e-e631-41bc-b794-40428157b41c) 85 | 86 | ### 授权后获取令牌 87 | ![Snipaste_2023-10-02_06-17-00](https://github.com/FoskyM/flarum-oauth-center/assets/39661663/52f1b984-345b-4e09-8b8c-dcdf39712fb3) 88 | 89 | ### 使用令牌获取资源 (使用 get 或 header 方式) 90 | ![Snipaste_2023-10-02_06-17-29](https://github.com/FoskyM/flarum-oauth-center/assets/39661663/aa79ad3b-a480-4d09-9159-359be4518f4b) 91 | ![Snipaste_2023-10-02_06-17-42](https://github.com/FoskyM/flarum-oauth-center/assets/39661663/5054ac5b-da79-4db3-9703-94e10a1cde5f) 92 | -------------------------------------------------------------------------------- /src/Middlewares/ResourceScopeAuthMiddleware.php: -------------------------------------------------------------------------------- 1 | settings = $settings; 29 | } 30 | public function process(Request $request, RequestHandlerInterface $handler): Response 31 | { 32 | if (!$request->getAttribute('originalUri')) { 33 | return $handler->handle($request); 34 | } 35 | 36 | $headerLine = $request->getHeaderLine('authorization'); 37 | 38 | $parts = explode(';', $headerLine); 39 | 40 | if (isset($parts[0]) && Str::startsWith($parts[0], self::TOKEN_PREFIX)) { 41 | $token = substr($parts[0], strlen(self::TOKEN_PREFIX)); 42 | } else { 43 | $token = Arr::get($request->getQueryParams(), 'access_token', ''); 44 | } 45 | $path = $request->getAttribute('originalUri')->getPath(); 46 | 47 | if ($token !== '' && $scopes = Scope::get_path_scope($path)) { 48 | try { 49 | $oauth = new OAuth($this->settings); 50 | $server = $oauth->server(); 51 | $oauth_request = $oauth->request()::createFromGlobals(); 52 | $has_scope = false; 53 | $filtered_scopes = []; 54 | foreach ($scopes as $scope) { 55 | if (strtolower($request->getMethod()) === strtolower($scope->method)) { 56 | if ($server->verifyResourceRequest($oauth_request, null, $scope->scope)) { 57 | $token = $server->getAccessTokenData($oauth_request); 58 | if (!in_array($scope->scope, explode(' ', $token['scope']))) { 59 | continue; 60 | } 61 | $filtered_scopes[] = $scope; 62 | $has_scope = true; 63 | } 64 | } 65 | } 66 | if (!$has_scope) { 67 | return new JsonResponse(json_decode($server->getResponse()->getResponseBody(), true)); 68 | } 69 | $actor = User::find($token['user_id']); 70 | $request = RequestUtil::withActor($request, $actor); 71 | $request = $request->withAttribute('oauth.scopes', $filtered_scopes); 72 | $request = $request->withAttribute('bypassCsrfToken', true); 73 | $request = $request->withoutAttribute('session'); 74 | 75 | } catch (ValidationException $exception) { 76 | 77 | $handler = resolve(IlluminateValidationExceptionHandler::class); 78 | 79 | $error = $handler->handle($exception); 80 | 81 | return (new JsonApiFormatter())->format($error, $request); 82 | } 83 | 84 | } 85 | 86 | return $handler->handle($request); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /js/src/admin/components/EditScopeModal.js: -------------------------------------------------------------------------------- 1 | import app from 'flarum/admin/app'; 2 | import Modal from 'flarum/common/components/Modal'; 3 | import Button from 'flarum/common/components/Button'; 4 | import Stream from 'flarum/common/utils/Stream'; 5 | import Select from 'flarum/common/components/Select'; 6 | import Checkbox from 'flarum/common/components/Checkbox'; 7 | 8 | export default class EditScopeModal extends Modal { 9 | oninit(vnode) { 10 | super.oninit(vnode); 11 | 12 | this.scope = this.attrs.scope; 13 | this.fields = [ 14 | 'scope', 15 | 'resource_path', 16 | 'method', 17 | 'is_default', 18 | 'scope_name', 19 | 'scope_icon', 20 | 'scope_desc', 21 | 22 | 'visible_fields', 23 | ]; 24 | 25 | this.values = this.fields.reduce((values, key) => { 26 | values[key] = Stream(this.scope[key]() || ''); 27 | return values; 28 | }, {}); 29 | } 30 | 31 | className() { 32 | return 'EditScopeModal Modal--large'; 33 | } 34 | 35 | title() { 36 | return app.translator.trans('foskym-oauth-center.admin.scopes.edit_scope'); 37 | } 38 | 39 | content() { 40 | return ( 41 |
42 |
43 |
44 |
45 | {this.renderFormGroups(this.fields.slice(0, 4))} 46 |
47 |
48 | {this.renderFormGroups(this.fields.slice(4, 7))} 49 |
50 |
51 | 52 |
53 | 54 | 57 |
58 | 59 |
60 | {Button.component({ 61 | type: 'submit', 62 | className: 'Button Button--primary Button--block EditScopeModal-save', 63 | loading: this.loading, 64 | }, app.translator.trans('core.admin.settings.submit_button'))} 65 |
66 |
67 |
68 | ); 69 | } 70 | 71 | onsubmit(e) { 72 | e.preventDefault(); 73 | 74 | this.loading = true; 75 | 76 | const scopeData = this.fields.reduce((data, key) => { 77 | data[key] = this.values[key](); 78 | return data; 79 | }, {}); 80 | 81 | this.scope.save(scopeData).then(() => { 82 | this.loading = false; 83 | m.redraw(); 84 | this.hide(); 85 | }); 86 | } 87 | 88 | renderFormGroups(fields) { 89 | return fields.map(key => 90 |
91 | 92 | {key === 'method' ? Select.component({ 93 | options: { 94 | 'GET': 'GET', 95 | 'POST': 'POST', 96 | 'PUT': 'PUT', 97 | 'DELETE': 'DELETE', 98 | 'PATCH': 'PATCH', 99 | }, 100 | value: this.values[key](), 101 | onchange: this.values[key], 102 | }) : key === 'is_default' ? Checkbox.component({ 103 | className: 'OAuthCenter-Checkbox', 104 | state: this.values[key]() === 1 || false, 105 | onchange: (checked) => this.values[key](checked ? 1 : 0), 106 | }) : } 107 |
108 | ) 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /extend.php: -------------------------------------------------------------------------------- 1 | js(__DIR__.'/js/dist/forum.js') 26 | ->css(__DIR__.'/less/forum.less') 27 | ->route('/oauth/authorize', 'oauth.authorize'), 28 | 29 | (new Extend\Frontend('admin')) 30 | ->js(__DIR__.'/js/dist/admin.js') 31 | ->css(__DIR__.'/less/admin.less'), 32 | new Extend\Locales(__DIR__.'/locale'), 33 | 34 | (new Extend\Routes('forum')) 35 | ->post('/oauth/authorize', 'oauth.authorize.post', Controllers\AuthorizeController::class) 36 | ->post('/oauth/authorize/fetch', 'oauth.authorize.fetch', Controllers\AuthorizeFetchController::class) 37 | ->post('/oauth/token', 'oauth.token', Controllers\TokenController::class), 38 | 39 | (new Extend\Routes('api')) 40 | ->get('/oauth-clients', 'oauth.clients.list', Api\Controller\ListClientController::class) 41 | ->post('/oauth-clients', 'oauth.clients.create', Api\Controller\CreateClientController::class) 42 | ->get('/oauth-clients/{client_id}', 'oauth.clients.show', Api\Controller\ShowClientController::class) 43 | ->patch('/oauth-clients/{id}', 'oauth.clients.update', Api\Controller\UpdateClientController::class) 44 | ->delete('/oauth-clients/{id}', 'oauth.clients.delete', Api\Controller\DeleteClientController::class) 45 | 46 | ->get('/oauth-scopes', 'oauth.scopes.list', Api\Controller\ListScopeController::class) 47 | ->post('/oauth-scopes', 'oauth.scopes.create', Api\Controller\CreateScopeController::class) 48 | ->patch('/oauth-scopes/{id}', 'oauth.scopes.update', Api\Controller\UpdateScopeController::class) 49 | ->delete('/oauth-scopes/{id}', 'oauth.scopes.delete', Api\Controller\DeleteScopeController::class) 50 | 51 | ->post('/oauth-tokens', 'oauth.tokens.create', Api\Controller\CreateTokenController::class) 52 | ->delete('/oauth-tokens/expired', 'oauth.tokens.delete.expired', Api\Controller\DeleteExpiredTokenController::class) 53 | 54 | ->get('/oauth-records', 'oauth.records.list', Api\Controller\ListRecordController::class) 55 | ->delete('/oauth-records/all', 'oauth.records.delete.all', Api\Controller\DeleteAllRecordController::class) 56 | ->delete('/oauth-records/user', 'oauth.records.delete.user', Api\Controller\DeleteUserRecordController::class) 57 | ->delete('/oauth-records/{id}', 'oauth.records.delete', Api\Controller\DeleteRecordController::class) 58 | 59 | ->get('/user', 'user.show', Controllers\ApiUserController::class), 60 | 61 | (new Extend\Settings) 62 | ->serializeToForum('foskym-oauth-center.display_mode', 'foskym-oauth-center.display_mode') 63 | ->serializeToForum('foskym-oauth-center.allow_implicit', 'foskym-oauth-center.allow_implicit', 'boolval') 64 | ->serializeToForum('foskym-oauth-center.enforce_state', 'foskym-oauth-center.enforce_state', 'boolval') 65 | ->serializeToForum('foskym-oauth-center.require_exact_redirect_uri', 'foskym-oauth-center.require_exact_redirect_uri', 'boolval') 66 | ->serializeToForum('foskym-oauth-center.authorization_method_fetch', 'foskym-oauth-center.authorization_method_fetch', 'boolval') 67 | ->serializeToForum('foskym-oauth-center.allow_delete_records', 'foskym-oauth-center.allow_delete_records', 'boolval'), 68 | 69 | (new Extend\Middleware('api')) 70 | ->insertAfter(AuthenticateWithHeader::class, ResourceScopeAuthMiddleware::class) 71 | ->add(ResourceScopeFieldsMiddleware::class), 72 | (new Extend\Middleware('forum')) 73 | ->insertBefore(CheckCsrfToken::class, UnsetCsrfMiddleware::class) 74 | ->insertAfter(CheckCsrfToken::class, UserCredentialsMiddleware::class), 75 | ]; 76 | -------------------------------------------------------------------------------- /docs/en.md: -------------------------------------------------------------------------------- 1 | ### setting 2 | ![Snipaste_2023-10-02_06-15-33](https://github.com/FoskyM/flarum-oauth-center/assets/39661663/0e8352bf-0aeb-4605-bd84-aaedf8cae0e8) 3 | 4 | - `Allow Implicit Grant` a way to return token directly to client, you can google it 5 | - `Enforce State Validation` `state` must be provided 6 | - `Require Exact Redirect URI` url in `redirect_uri` should be exactly the same as the one of client 7 | - `Access Token Lifetime` 8 | 9 | ### create a client 10 | ![Snipaste_2023-10-02_06-15-52](https://github.com/FoskyM/flarum-oauth-center/assets/39661663/e9879852-f4ec-4a04-9f81-58004692493c) 11 | #### instructions 12 | - `Name` name of client 13 | - `Description` description of client 14 | - `Icon` icon of client, optional 15 | - `Homepage` homepage of client, optional 16 | - `Redirect URI` redirect uri of client, required, multiple uri should be separated by space (not recommended) 17 | - `Scopes` optional (don't fill it if you don't know) 18 | - `Grant Types` optional (don't fill it if you don't know) 19 | - `Client ID` and `Client Secret` used for client authentication, generated automatically, don't share it with others 20 | 21 | ### set scope for your resources (user.read is default scope) 22 | ![Snipaste_2023-10-02_06-16-06](https://github.com/FoskyM/flarum-oauth-center/assets/39661663/31648ad2-4326-47e0-9d26-c0e3b2f30f8d) 23 | most people only need `user.read` scope, if you need more, you can add it here (maybe you need to know something about OAuth scope first) 24 | #### instructions 25 | - `Scope ID` unique identifier of scope, used for distinguish, you can refer to `Github` scope 26 | - `Resource Path` resource path of scope 27 | - `Request Method` request method of resource path, usually `GET` 28 | - `Default` if checked, this scope will be added even if it's not in `scope` parameter 29 | - `Name` name of scope, used for display 30 | - `Description` description of scope, used for display, you can use `{user}` `{client_name}` variable to represent user and client name 31 | - `Icon` support `FontAwesome` icon and normal image 32 | 33 | ### uri 34 | #### authorize 35 | `/oauth/authorize` 36 | 37 | | param | description | required | default | example | 38 | | --- | --- | --- | --- | --- | 39 | | client_id | client id | yes | none | 123456 | 40 | | response_type | grant type | yes | none | code or token | 41 | | redirect_uri | redirect uri | yes | client redirect uri | https://example.com/oauth/callback | 42 | | scope | scope | no | none | user.read | 43 | | state | state | no | none | 123456 | 44 | 45 | example: 46 | ``` 47 | GET https://example.com/oauth/authorize?client_id=123456&response_type=code&redirect_uri=https://user.example.com/oauth/callback&scope=user.read&state=123456 48 | ``` 49 | 50 | #### token 51 | `/oauth/token` 52 | 53 | | param | description | required | default | example | 54 | | --- | --- | --- | --- | --- | 55 | | client_id | client id | yes | none | 123456 | 56 | | client_secret | client secret | yes | none | 123456 | 57 | | grant_type | grant type | yes | none | authorization_code or refresh_token | 58 | | code | authorization code | required when grant type is authorization_code | none | 123456 | 59 | | refresh_token | refresh token | required when grant type is refresh_token | none | 123456 | 60 | | redirect_uri | redirect uri | required when grant type is authorization_code | client redirect uri | https://example.com/oauth/callback | 61 | 62 | example: 63 | ``` 64 | POST https://example.com/oauth/token 65 | 66 | Payload: client_id=123456&client_secret=123456&grant_type=authorization_code&code=123456&redirect_uri=https://example.com/oauth/callback 67 | ``` 68 | 69 | ### resource 70 | `/api/user` 71 | 72 | | param | description | required | default | example | 73 | | --- |--------------|-----|---------| --- | 74 | | access_token | access token | yes | none | 123456 | 75 | 76 | ### example 77 | 78 | ``` 79 | GET https://example.com/api/user?access_token=123456 80 | ``` 81 | 82 | ### do it as normal OAuth client 83 | ![Snipaste_2023-10-02_06-16-31](https://github.com/FoskyM/flarum-oauth-center/assets/39661663/1632672e-e631-41bc-b794-40428157b41c) 84 | 85 | ### get access token after authorized 86 | ![Snipaste_2023-10-02_06-17-00](https://github.com/FoskyM/flarum-oauth-center/assets/39661663/52f1b984-345b-4e09-8b8c-dcdf39712fb3) 87 | 88 | ### using token to access resources (get or header) 89 | ![Snipaste_2023-10-02_06-17-29](https://github.com/FoskyM/flarum-oauth-center/assets/39661663/aa79ad3b-a480-4d09-9159-359be4518f4b) 90 | ![Snipaste_2023-10-02_06-17-42](https://github.com/FoskyM/flarum-oauth-center/assets/39661663/5054ac5b-da79-4db3-9703-94e10a1cde5f) 91 | -------------------------------------------------------------------------------- /js/src/forum/components/user/AuthorizedPage.js: -------------------------------------------------------------------------------- 1 | import app from 'flarum/forum/app'; 2 | import UserPage from 'flarum/forum/components/UserPage'; 3 | import LoadingIndicator from 'flarum/common/components/LoadingIndicator'; 4 | import Placeholder from 'flarum/common/components/Placeholder'; 5 | import Button from 'flarum/common/components/Button'; 6 | 7 | export default class AuthorizedPage extends UserPage { 8 | records = []; 9 | loading = true; 10 | nomore = false; 11 | page = 0; 12 | oninit(vnode) { 13 | super.oninit(vnode); 14 | this.loadUser(m.route.param('username')); 15 | this.loadRecords(); 16 | } 17 | loadRecords() { 18 | app.store.find('oauth-records', { page: this.page }).then((records) => { 19 | this.records = this.records.concat(records); 20 | this.loading = false; 21 | if (records.length < 10) { 22 | this.nomore = true; 23 | } 24 | m.redraw(); 25 | }); 26 | } 27 | loadMore() { 28 | this.loadRecords((this.page += 1)); 29 | } 30 | content() { 31 | if (this.records.length === 0) { 32 | return ; 33 | } 34 | 35 | let allow_delete = app.forum.attribute('foskym-oauth-center.allow_delete_records'); 36 | console.log(allow_delete); 37 | 38 | return ( 39 |
40 | {allow_delete && ( 41 | 59 | )} 60 | 61 |
    62 | {this.records.map((record) => ( 63 |
  • 64 |
    65 |
    66 | client_icon 67 |
    68 |

    69 | 70 | {record.attribute('client').client_name} 71 | 72 |

    73 |

    {record.attribute('client').client_desc}

    74 |
    75 |
    76 |
    77 | 78 | 79 | {allow_delete && ( 80 | 91 | )} 92 |
    93 |
    94 |
    95 |
  • 96 | ))} 97 |
98 | 99 | {this.loading && } 100 | 101 | {!this.loading && !this.nomore && ( 102 |
103 | 106 |
107 | )} 108 | 109 | {this.nomore && } 110 |
111 | ); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /less/forum/oauth.less: -------------------------------------------------------------------------------- 1 | body:has(.oauth-area) { 2 | overflow-y: auto; 3 | } 4 | 5 | .oauth-area { 6 | display: block !important; 7 | position: relative; 8 | left: 0; 9 | top: 0; 10 | padding: 110px 0; 11 | min-height: 100%; 12 | box-sizing: border-box; 13 | h3 { 14 | margin-block-start: 0; 15 | margin-block-end: 0; 16 | color: @heading-color; 17 | } 18 | } 19 | 20 | .oauth-header, .oauth-body { 21 | padding: 20px; 22 | background-color: @control-bg; 23 | } 24 | 25 | .oauth-header { 26 | backdrop-filter: blur(0); 27 | text-align: center; 28 | border-radius: @border-radius @border-radius 0 0; 29 | h2 { 30 | margin-bottom: 8px; 31 | font-weight: 600; 32 | font-size: 40px; 33 | color: @heading-color; 34 | } 35 | p { 36 | font-weight: 400; 37 | font-size: 20px; 38 | color: @control-color; 39 | } 40 | } 41 | 42 | .oauth-body { 43 | border-radius: 0 0 @border-radius @border-radius; 44 | .oauth-form-item { 45 | position: relative; 46 | margin-bottom: 15px; 47 | clear: both; 48 | *zoom: 1; 49 | &:after { 50 | content: '\20'; 51 | clear: both; 52 | *zoom: 1; 53 | display: block; 54 | height: 0; 55 | } 56 | } 57 | } 58 | 59 | 60 | .oauth-icon { 61 | position: absolute; 62 | left: 4px; 63 | top: 1px; 64 | width: auto; 65 | line-height: 35px; 66 | text-align: center; 67 | color: @control-color; 68 | padding: 0 8px; 69 | font-size: 14px; 70 | } 71 | 72 | label:before { 73 | color: @control-color; 74 | } 75 | 76 | .oauth-scope-area { 77 | padding-top: 10px; 78 | padding-bottom: 10px; 79 | overflow: auto; 80 | max-height: 350px; 81 | position: relative; 82 | } 83 | 84 | .oauth-scope { 85 | margin-top: 15px; 86 | &:first-child { 87 | margin-top: 0; 88 | } 89 | } 90 | 91 | .oauth-scope, .oauth-scope-body { 92 | overflow: hidden; 93 | zoom: 1; 94 | } 95 | 96 | .oauth-scope-body, .oauth-scope-left, .oauth-scope-right { 97 | display: table-cell; 98 | vertical-align: top; 99 | } 100 | 101 | .oauth-scope-left, .oauth-scope > .pull-left { 102 | padding-right: 10px; 103 | min-width: 42px; 104 | text-align: center; 105 | } 106 | 107 | img.oauth-scope-object { 108 | display: block; 109 | vertical-align: middle; 110 | border: 0; 111 | width: 32px; 112 | height: 32px; 113 | } 114 | 115 | i.oauth-scope-object { 116 | color: @heading-color; 117 | } 118 | 119 | .oauth-scope-body { 120 | width: 10000px; 121 | padding-left: 8px; 122 | } 123 | 124 | .oauth-scope-heading { 125 | margin-top: 0; 126 | font-weight: 800; 127 | color: @heading-color; 128 | margin-block-end: 0; 129 | font-size: 12px; 130 | } 131 | 132 | .oauth-scope-body small { 133 | font-weight: 500; 134 | font-size: 12px; 135 | color: @control-color; 136 | } 137 | 138 | .oauth-btn-group { 139 | display: flex; 140 | margin-top: 15px; 141 | } 142 | 143 | .oauth-info { 144 | text-align: center; 145 | padding-bottom: 20px; 146 | position: relative; 147 | img { 148 | width: 96px; 149 | border-radius: 50%; 150 | box-shadow: 1px 0 0 0 #e8e8e8, 0 1px 0 0 #e8e8e8, 1px 1px 0 0 #e8e8e8, inset 1px 0 0 0 #e8e8e8, inset 0 1px 0 0 #e8e8e8; 151 | transition: all .3s; 152 | } 153 | img:hover { 154 | box-shadow: 0 2px 8px @shadow-color; 155 | } 156 | i { 157 | top: -36px; 158 | position: relative; 159 | padding: 0 10px; 160 | color: @control-color; 161 | } 162 | span { 163 | display: none; 164 | } 165 | } 166 | 167 | .oauth-user { 168 | width: 100%; 169 | text-align: center; 170 | display: flex; 171 | flex-wrap: wrap; 172 | justify-content: center; 173 | padding-bottom: 20px; 174 | position: relative; 175 | .oauth-username { 176 | display: grid; 177 | padding-left: 10px; 178 | padding-top: 2px; 179 | text-align: left; 180 | } 181 | } 182 | 183 | 184 | @media (min-width: 768px) { 185 | .oauth-user { 186 | display: none; 187 | } 188 | } 189 | 190 | @import "box.less"; 191 | @import "card.less"; 192 | @import "column.less"; 193 | 194 | 195 | .oauth-box, .oauth-card { 196 | @media screen and (max-width: 768px) { 197 | .oauth-area { 198 | padding-top: 60px 199 | } 200 | 201 | .oauth-main { 202 | width: 300px 203 | } 204 | 205 | .oauth-header, .oauth-body { 206 | padding: 10px 207 | } 208 | } 209 | 210 | @media screen and (max-width: 600px) { 211 | .oauth-area { 212 | padding-top: 0 213 | } 214 | 215 | .oauth-main { 216 | width: 100%; 217 | } 218 | 219 | .oauth-main::before { 220 | box-shadow: none !important; 221 | } 222 | 223 | .oauth-body { 224 | box-shadow: 0 5px 10px -5px @shadow-color; 225 | } 226 | } 227 | } 228 | 229 | .oauth-card, .oauth-column { 230 | .oauth-scope-area { 231 | padding-top: 0; 232 | width: 30%; 233 | } 234 | 235 | .oauth-info, .oauth-form { 236 | width: 60%; 237 | } 238 | 239 | @media screen and (max-width: 768px) { 240 | .oauth-info, .oauth-form, .oauth-scope-area { 241 | width: 100%; 242 | } 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /js/src/admin/pages/IndexPage.js: -------------------------------------------------------------------------------- 1 | import app from 'flarum/admin/app'; 2 | import Page from 'flarum/common/components/Page'; 3 | import FieldSet from 'flarum/common/components/FieldSet'; 4 | import Button from 'flarum/common/components/Button'; 5 | import saveSettings from 'flarum/admin/utils/saveSettings'; 6 | import Stream from 'flarum/common/utils/Stream'; 7 | import Select from 'flarum/common/components/Select'; 8 | import Switch from 'flarum/common/components/Switch'; 9 | 10 | export default class IndexPage extends Page { 11 | oninit(vnode) { 12 | super.oninit(vnode); 13 | 14 | this.saving = false; 15 | 16 | this.fields = [ 17 | 'display_mode', 18 | 'access_lifetime', 19 | 20 | 'allow_delete_records', 21 | 'allow_implicit', 22 | 'enforce_state', 23 | 'require_exact_redirect_uri', 24 | 'authorization_method_fetch', 25 | ]; 26 | const settings = app.data.settings; 27 | this.values = this.fields.reduce((values, key) => { 28 | key = 'foskym-oauth-center.' + key; 29 | values[key] = Stream(settings[key] || ''); 30 | return values; 31 | }, {}); 32 | 33 | this.values['foskym-oauth-center.display_mode'] = this.values['foskym-oauth-center.display_mode']() || 'box'; 34 | 35 | for (let i = 2; i < this.fields.length; i++) { 36 | this.values['foskym-oauth-center.' + this.fields[i]] = settings['foskym-oauth-center.' + this.fields[i]] === '1'; 37 | } 38 | } 39 | 40 | view() { 41 | return ( 42 |
43 |
44 | {this.fields.slice(2).map((field) => 45 | FieldSet.component({}, [ 46 |
, 47 | Switch.component( 48 | { 49 | state: this.values['foskym-oauth-center.' + field], 50 | onchange: (value) => this.saveSingleSetting(field, value), 51 | loading: this.saving, 52 | }, 53 | app.translator.trans(`foskym-oauth-center.admin.settings.${field}`) 54 | ), 55 | ]) 56 | )} 57 |
58 | {FieldSet.component({}, [ 59 | Select.component({ 60 | options: { 61 | box: 'Box', 62 | card: 'Card', 63 | column: 'Column', 64 | }, 65 | value: this.values['foskym-oauth-center.' + this.fields[0]], 66 | onchange: (value) => this.saveSingleSetting(this.fields[0], value), 67 | loading: this.saving, 68 | }), 69 |
{app.translator.trans(`foskym-oauth-center.admin.settings.${this.fields[0]}`)}
, 70 | ])} 71 |
72 | {FieldSet.component({}, [ 73 | , 79 |
{app.translator.trans(`foskym-oauth-center.admin.settings.${this.fields[1]}`)}
, 80 | Button.component( 81 | { 82 | type: 'submit', 83 | className: 'Button Button--primary', 84 | loading: this.saving, 85 | }, 86 | app.translator.trans('core.admin.settings.submit_button') 87 | ), 88 | ])} 89 |
90 |
91 |
92 | 112 |
113 |
114 | ); 115 | } 116 | 117 | saveSingleSetting(setting, value) { 118 | if (this.saving) return; 119 | 120 | this.saving = true; 121 | 122 | this.values['foskym-oauth-center.' + setting] = value; 123 | 124 | saveSettings({ ['foskym-oauth-center.' + setting]: value }) 125 | .then(() => app.alerts.show({ type: 'success' }, app.translator.trans('core.admin.settings.saved_message'))) 126 | .catch(() => {}) 127 | .finally(() => { 128 | this.saving = false; 129 | m.redraw(); 130 | }); 131 | } 132 | 133 | onsubmit(e) { 134 | e.preventDefault(); 135 | 136 | if (this.saving) return; 137 | 138 | this.saving = true; 139 | 140 | const settings = {}; 141 | 142 | settings['foskym-oauth-center.access_lifetime'] = this.values['foskym-oauth-center.access_lifetime'](); 143 | 144 | if (settings['foskym-oauth-center.access_lifetime'] === '') { 145 | settings['foskym-oauth-center.access_lifetime'] = 3600; 146 | } 147 | 148 | saveSettings(settings) 149 | .then(() => app.alerts.show({ type: 'success' }, app.translator.trans('core.admin.settings.saved_message'))) 150 | .catch(() => {}) 151 | .finally(() => { 152 | this.saving = false; 153 | m.redraw(); 154 | }); 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /js/src/forum/components/oauth/AuthorizePage.js: -------------------------------------------------------------------------------- 1 | import app from 'flarum/forum/app'; 2 | import {extend} from 'flarum/common/extend'; 3 | import Page from 'flarum/common/components/Page'; 4 | import IndexPage from 'flarum/forum/components/IndexPage'; 5 | import LogInModal from 'flarum/forum/components/LogInModal'; 6 | import extractText from 'flarum/common/utils/extractText'; 7 | import Tooltip from 'flarum/common/components/Tooltip'; 8 | import Button from 'flarum/common/components/Button'; 9 | import LoadingIndicator from 'flarum/common/components/LoadingIndicator'; 10 | import avatar from 'flarum/common/helpers/avatar'; 11 | 12 | import ScopeComponent from '../ScopeComponent'; 13 | 14 | export default class AuthorizePage extends IndexPage { 15 | params = []; 16 | client = null; 17 | scopes = null; 18 | client_scope = []; 19 | loading = true; 20 | is_authorized = false; 21 | submit_loading = false; 22 | display_mode = 'box'; 23 | 24 | oninit(vnode) { 25 | super.oninit(vnode); 26 | if (!app.session.user) { 27 | setTimeout(() => app.modal.show(LogInModal), 500); 28 | } 29 | 30 | this.display_mode = app.forum.attribute('foskym-oauth-center.display_mode') || 'box'; 31 | 32 | const params = m.route.param(); 33 | 34 | if (params.client_id == null || params.response_type == null || params.redirect_uri == null) { 35 | m.route.set('/'); 36 | return; 37 | } 38 | 39 | this.params = params; 40 | 41 | Promise.all([ 42 | app.store.find('oauth-clients', params.client_id), 43 | app.store.find('oauth-scopes') 44 | ]).then(([client, scopes]) => { 45 | if (!client) { 46 | m.route.set('/'); 47 | return; 48 | } 49 | 50 | this.client = client; 51 | this.scopes = scopes; 52 | 53 | let uris = this.client.redirect_uri().split(' '); 54 | 55 | if (app.forum.attribute('foskym-oauth-center.require_exact_redirect_uri') && !uris.includes(params.redirect_uri)) { 56 | m.route.set('/'); 57 | return; 58 | } 59 | 60 | if (!app.forum.attribute('foskym-oauth-center.allow_implicit') && params.response_type === 'token') { 61 | m.route.set('/'); 62 | return; 63 | } 64 | 65 | if (app.forum.attribute('foskym-oauth-center.enforce_state') && params.state == null) { 66 | m.route.set('/'); 67 | return; 68 | } 69 | 70 | let scopes_temp = params.scope ? params.scope.split(' ') : (this.client.scope() || '').split(' '); 71 | 72 | let default_scopes = this.scopes.filter(scope => scope.is_default() === 1).map(scope => scope.scope()); 73 | 74 | this.client_scope = scopes_temp.filter((scope, index) => scopes_temp.indexOf(scope) === index); 75 | this.client_scope = this.client_scope.concat(default_scopes).filter(scope => scope !== ''); 76 | 77 | this.loading = false; 78 | m.redraw(); 79 | }); 80 | } 81 | 82 | setTitle() { 83 | app.setTitle(extractText(app.translator.trans('foskym-oauth-center.forum.page.title.authorize'))); 84 | app.setTitleCount(0); 85 | } 86 | 87 | view() { 88 | if (!this.client || this.loading) { 89 | return ; 90 | } 91 | return ( 92 |
93 |
94 |
95 |
96 |
97 |

{app.forum.attribute('title')}

98 |

99 | {app.translator.trans('foskym-oauth-center.forum.authorize.access')} 100 | {this.client.client_name()} 101 | 102 |

103 | 104 |
105 |
106 | 107 |
108 | {avatar(app.session.user, {className: 'oauth-avatar'})} 109 |
110 | {app.session.user.username()} 111 | {app.session.user.displayName()} 112 |
113 |
114 | 115 |
116 | 117 | favicon 118 | 119 | 120 | 121 | client_icon 122 | 123 | {this.client.client_name()} 124 |
125 |
126 |

{app.translator.trans('foskym-oauth-center.forum.authorize.require_these_scopes')}

127 | { 128 | this.client_scope 129 | .filter(scope => scope) 130 | .map(scope => { 131 | let scope_info = this.scopes.find(s => s.scope() === scope); 132 | return scope_info && ; 133 | }) 134 | } 135 |
136 |
137 | {Object.keys(this.params).map(key => ( 138 | 139 | ))} 140 | 141 |
142 | 146 | 150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 | ); 158 | } 159 | deny(e) { 160 | this.is_authorized = false; 161 | } 162 | agree(e) { 163 | this.is_authorized = true; 164 | } 165 | 166 | onsubmit(e) { 167 | e.preventDefault(); 168 | this.submit_loading = true; 169 | if (app.forum.attribute('foskym-oauth-center.authorization_method_fetch')) { 170 | app.request({ 171 | method: 'POST', 172 | url: '/oauth/authorize/fetch', 173 | body: { 174 | ...this.params, 175 | is_authorized: this.is_authorized, 176 | } 177 | }).then((params) => { 178 | window.location.href = params.location; 179 | }); 180 | } else { 181 | e.target.submit(); 182 | } 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /js/dist/forum.js: -------------------------------------------------------------------------------- 1 | (()=>{var t={n:e=>{var o=e&&e.__esModule?()=>e.default:()=>e;return t.d(o,{a:o}),o},d:(e,o)=>{for(var r in o)t.o(o,r)&&!t.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:o[r]})},o:(t,e)=>Object.prototype.hasOwnProperty.call(t,e),r:t=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})}},e={};(()=>{"use strict";t.r(e),t.d(e,{extend:()=>h});const o=flarum.core.compat["common/extenders"];var r=t.n(o);function n(t,e){return n=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(t,e){return t.__proto__=e,t},n(t,e)}function a(t,e){t.prototype=Object.create(e.prototype),t.prototype.constructor=t,n(t,e)}const i=flarum.core.compat["common/Model"];var s=t.n(i),c=function(t){function e(){for(var e,o=arguments.length,r=new Array(o),n=0;n-1?m("i",{class:"oauth-scope-object fa-2x "+e.scope_icon(),style:"margin-left:2px;"}):m("img",{class:"oauth-scope-object",src:e.scope_icon(),style:"width:32px"})),m("div",{class:"oauth-scope-body"},m("h6",{class:"oauth-scope-heading"},e.scope_name()),m("small",null,e.scope_desc().replace("{client_name}",o.client_name()).replace("{user}",f().session.user.attribute("displayName")))))},e}(t.n(q)()),I=function(t){function e(){for(var e,o=arguments.length,r=new Array(o),n=0;n{var t={n:e=>{var n=e&&e.__esModule?()=>e.default:()=>e;return t.d(n,{a:n}),n},d:(e,n)=>{for(var o in n)t.o(n,o)&&!t.o(e,o)&&Object.defineProperty(e,o,{enumerable:!0,get:n[o]})},o:(t,e)=>Object.prototype.hasOwnProperty.call(t,e),r:t=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})}},e={};(()=>{"use strict";t.r(e),t.d(e,{extend:()=>h});const n=flarum.core.compat["common/extenders"];var o=t.n(n);function s(t,e){return s=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(t,e){return t.__proto__=e,t},s(t,e)}function i(t,e){t.prototype=Object.create(e.prototype),t.prototype.constructor=t,s(t,e)}const a=flarum.core.compat["common/Model"];var r=t.n(a),c=function(t){function e(){for(var e,n=arguments.length,o=new Array(n),s=0;sfirst(); 46 | 47 | return $client && $client['client_secret'] == $client_secret; 48 | } 49 | 50 | /** 51 | * @param string $client_id 52 | * @return bool 53 | */ 54 | public function isPublicClient($client_id) 55 | { 56 | $client = Models\Client::where('client_id', $client_id)->first(); 57 | 58 | if (!$client) { 59 | return false; 60 | } 61 | 62 | return empty($client['client_secret']); 63 | } 64 | 65 | /** 66 | * @param string $client_id 67 | * @return array|mixed 68 | */ 69 | public function getClientDetails($client_id) 70 | { 71 | $client = Models\Client::where('client_id', $client_id)->first(); 72 | 73 | return $client; 74 | } 75 | 76 | /** 77 | * @param string $client_id 78 | * @param null|string $client_secret 79 | * @param null|string $redirect_uri 80 | * @param null|array $grant_types 81 | * @param null|string $scope 82 | * @param null|string $user_id 83 | * @return bool 84 | */ 85 | public function setClientDetails($client_id, $client_secret = null, $redirect_uri = null, $grant_types = null, $scope = null, $user_id = null) 86 | { 87 | // if it exists, update it. 88 | if ($this->getClientDetails($client_id)) { 89 | return Models\Client::where('client_id', $client_id)->update([ 90 | 'client_secret' => $client_secret, 91 | 'redirect_uri' => $redirect_uri, 92 | 'grant_types' => $grant_types, 93 | 'scope' => $scope, 94 | 'user_id' => $user_id, 95 | ]); 96 | } else { 97 | return Models\Client::create([ 98 | 'client_id' => $client_id, 99 | 'client_secret' => $client_secret, 100 | 'redirect_uri' => $redirect_uri, 101 | 'grant_types' => $grant_types, 102 | 'scope' => $scope, 103 | 'user_id' => $user_id, 104 | ]); 105 | } 106 | } 107 | 108 | /** 109 | * @param $client_id 110 | * @param $grant_type 111 | * @return bool 112 | */ 113 | public function checkRestrictedGrantType($client_id, $grant_type) 114 | { 115 | $details = $this->getClientDetails($client_id); 116 | if (isset($details['grant_types'])) { 117 | $grant_types = explode(' ', $details['grant_types']); 118 | 119 | return in_array($grant_type, (array) $grant_types); 120 | } 121 | 122 | // if grant_types are not defined, then none are restricted 123 | return true; 124 | } 125 | 126 | /* OAuth2\Storage\AccessTokenInterface */ 127 | 128 | /** 129 | * @param string $access_token 130 | * @return array|bool|mixed|null 131 | */ 132 | public function getAccessToken($access_token) 133 | { 134 | if ($token = Models\AccessToken::where('access_token', $access_token)->first()) { 135 | $token['expires'] = strtotime($token['expires']); 136 | return $token; 137 | } 138 | return false; 139 | } 140 | 141 | /** 142 | * @param string $access_token 143 | * @param mixed $client_id 144 | * @param mixed $user_id 145 | * @param int $expires 146 | * @param string $scope 147 | * @return bool 148 | */ 149 | public function setAccessToken($access_token, $client_id, $user_id, $expires, $scope = null) 150 | { 151 | $expires = date('Y-m-d H:i:s', $expires); 152 | 153 | if ($this->getAccessToken($access_token)) { 154 | return Models\AccessToken::where('access_token', $access_token)->update([ 155 | 'client_id' => $client_id, 156 | 'user_id' => $user_id, 157 | 'expires' => $expires, 158 | 'scope' => $scope, 159 | ]); 160 | } else { 161 | return Models\AccessToken::create([ 162 | 'access_token' => $access_token, 163 | 'client_id' => $client_id, 164 | 'user_id' => $user_id, 165 | 'expires' => $expires, 166 | 'scope' => $scope, 167 | ]); 168 | } 169 | } 170 | 171 | /** 172 | * @param $access_token 173 | * @return bool 174 | */ 175 | public function unsetAccessToken($access_token) 176 | { 177 | return Models\AccessToken::where('access_token', $access_token)->delete(); 178 | } 179 | 180 | /* OAuth2\Storage\AuthorizationCodeInterface */ 181 | /** 182 | * @param string $code 183 | * @return mixed 184 | */ 185 | public function getAuthorizationCode($code) 186 | { 187 | if ($code = Models\AuthorizationCode::where('authorization_code', $code)->first()) { 188 | // convert date string back to timestamp 189 | $code['expires'] = strtotime($code['expires']); 190 | } 191 | 192 | return $code; 193 | } 194 | 195 | /** 196 | * @param string $code 197 | * @param mixed $client_id 198 | * @param mixed $user_id 199 | * @param string $redirect_uri 200 | * @param int $expires 201 | * @param string $scope 202 | * @param string $id_token 203 | * @return bool|mixed 204 | */ 205 | public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null, $code_challenge = null, $code_challenge_method = null) 206 | { 207 | /*if (func_num_args() > 6) { 208 | // we are calling with an id token 209 | return call_user_func_array(array($this, 'setAuthorizationCodeWithIdToken'), func_get_args()); 210 | }*/ 211 | 212 | // convert expires to datestring 213 | $expires = date('Y-m-d H:i:s', $expires); 214 | 215 | // if it exists, update it. 216 | if ($this->getAuthorizationCode($code)) { 217 | return Models\AuthorizationCode::where('authorization_code', $code)->update([ 218 | 'client_id' => $client_id, 219 | 'user_id' => $user_id, 220 | 'redirect_uri' => $redirect_uri, 221 | 'expires' => $expires, 222 | 'scope' => $scope 223 | ]); 224 | 225 | } else { 226 | return Models\AuthorizationCode::create([ 227 | 'authorization_code' => $code, 228 | 'client_id' => $client_id, 229 | 'user_id' => $user_id, 230 | 'redirect_uri' => $redirect_uri, 231 | 'expires' => $expires, 232 | 'scope' => $scope 233 | ]); 234 | } 235 | 236 | } 237 | 238 | /** 239 | * @param string $code 240 | * @param mixed $client_id 241 | * @param mixed $user_id 242 | * @param string $redirect_uri 243 | * @param string $expires 244 | * @param string $scope 245 | * @param string $id_token 246 | * @return bool 247 | */ 248 | private function setAuthorizationCodeWithIdToken($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null, $code_challenge = null, $code_challenge_method = null) 249 | { 250 | // convert expires to datestring 251 | $expires = date('Y-m-d H:i:s', $expires); 252 | 253 | // if it exists, update it. 254 | if ($this->getAuthorizationCode($code)) { 255 | return Models\AuthorizationCode::where('authorization_code', $code)->update([ 256 | 'client_id' => $client_id, 257 | 'user_id' => $user_id, 258 | 'redirect_uri' => $redirect_uri, 259 | 'expires' => $expires, 260 | 'scope' => $scope, 261 | 'id_token' => $id_token, 262 | 'code_challenge' => $code_challenge, 263 | 'code_challenge_method' => $code_challenge_method 264 | ]); 265 | } else { 266 | return Models\AuthorizationCode::create([ 267 | 'authorization_code' => $code, 268 | 'client_id' => $client_id, 269 | 'user_id' => $user_id, 270 | 'redirect_uri' => $redirect_uri, 271 | 'expires' => $expires, 272 | 'scope' => $scope, 273 | 'id_token' => $id_token, 274 | 'code_challenge' => $code_challenge, 275 | 'code_challenge_method' => $code_challenge_method 276 | ]); 277 | } 278 | } 279 | 280 | /** 281 | * @param string $code 282 | * @return bool 283 | */ 284 | public function expireAuthorizationCode($code) 285 | { 286 | return Models\AuthorizationCode::where('authorization_code', $code)->delete(); 287 | } 288 | 289 | /* OAuth2\Storage\RefreshTokenInterface */ 290 | 291 | /** 292 | * @param string $refresh_token 293 | * @return bool|mixed 294 | */ 295 | public function getRefreshToken($refresh_token) 296 | { 297 | $token = Models\RefreshToken::where('refresh_token', $refresh_token)->first(); 298 | 299 | if ($token) { 300 | // convert expires to epoch time 301 | $token['expires'] = strtotime($token['expires']); 302 | } 303 | 304 | return $token; 305 | } 306 | 307 | /** 308 | * @param string $refresh_token 309 | * @param mixed $client_id 310 | * @param mixed $user_id 311 | * @param string $expires 312 | * @param string $scope 313 | * @return bool 314 | */ 315 | public function setRefreshToken($refresh_token, $client_id, $user_id, $expires, $scope = null) 316 | { 317 | // convert expires to datestring 318 | $expires = date('Y-m-d H:i:s', $expires); 319 | 320 | return Models\RefreshToken::create([ 321 | 'refresh_token' => $refresh_token, 322 | 'client_id' => $client_id, 323 | 'user_id' => $user_id, 324 | 'expires' => $expires, 325 | 'scope' => $scope 326 | ]); 327 | } 328 | 329 | /** 330 | * @param string $refresh_token 331 | * @return bool 332 | */ 333 | public function unsetRefreshToken($refresh_token) 334 | { 335 | return Models\RefreshToken::where('refresh_token', $refresh_token)->delete(); 336 | } 337 | 338 | /* OAuth2\Storage\UserCredentialsInterface */ 339 | /** 340 | * @param string $username 341 | * @param string $password 342 | * @return bool 343 | */ 344 | public function checkUserCredentials($username, $password) 345 | { 346 | if ($user = User::where('username', $username)->first()) { 347 | return $user->checkPassword($password); 348 | } 349 | 350 | return false; 351 | } 352 | 353 | /** 354 | * @param string $username 355 | * @return array|bool 356 | */ 357 | public function getUserDetails($username) 358 | { 359 | $user = User::where('username', $username)->first(); 360 | $user->user_id = $user->id; 361 | 362 | return $user; 363 | } 364 | 365 | /** 366 | * @param mixed $user_id 367 | * @param string $claims 368 | * @return array|bool 369 | */ 370 | public function getUserClaims($user_id, $claims) 371 | { 372 | if (!$userDetails = $this->getUserDetails($user_id)) { 373 | return false; 374 | } 375 | 376 | $claims = explode(' ', trim($claims)); 377 | $userClaims = array(); 378 | 379 | // for each requested claim, if the user has the claim, set it in the response 380 | $validClaims = explode(' ', self::VALID_CLAIMS); 381 | foreach ($validClaims as $validClaim) { 382 | if (in_array($validClaim, $claims)) { 383 | if ($validClaim == 'address') { 384 | // address is an object with subfields 385 | $userClaims['address'] = $this->getUserClaim($validClaim, $userDetails['address'] ?: $userDetails); 386 | } else { 387 | $userClaims = array_merge($userClaims, $this->getUserClaim($validClaim, $userDetails)); 388 | } 389 | } 390 | } 391 | 392 | return $userClaims; 393 | } 394 | 395 | /** 396 | * @param string $claim 397 | * @param array $userDetails 398 | * @return array 399 | */ 400 | protected function getUserClaim($claim, $userDetails) 401 | { 402 | $userClaims = array(); 403 | $claimValuesString = constant(sprintf('self::%s_CLAIM_VALUES', strtoupper($claim))); 404 | $claimValues = explode(' ', $claimValuesString); 405 | 406 | foreach ($claimValues as $value) { 407 | $userClaims[$value] = isset($userDetails[$value]) ? $userDetails[$value] : null; 408 | } 409 | 410 | return $userClaims; 411 | } 412 | 413 | /* OAuth2\Storage\ScopeInterface */ 414 | 415 | /** 416 | * @param string $scope 417 | * @return bool 418 | */ 419 | public function scopeExists($scope) 420 | { 421 | $scope = explode(' ', $scope); 422 | 423 | if ($count = Models\Scope::whereIn('scope', $scope)->count()) { 424 | return $count == count($scope); 425 | } 426 | 427 | return false; 428 | } 429 | 430 | /** 431 | * @param mixed $client_id 432 | * @return null|string 433 | */ 434 | public function getDefaultScope($client_id = null) 435 | { 436 | if ($result = Models\Scope::where('is_default', true)->get()) { 437 | $defaultScope = array_map(function ($row) { 438 | return $row['scope']; 439 | }, $result->toArray()); 440 | 441 | return implode(' ', $defaultScope); 442 | } 443 | 444 | return null; 445 | } 446 | 447 | /* OAuth2\Storage\JwtBearerInterface */ 448 | 449 | /** 450 | * @param mixed $client_id 451 | * @param $subject 452 | * @return string 453 | */ 454 | public function getClientKey($client_id, $subject) 455 | { 456 | $jwt = Models\Jwt::where('client_id', $client_id)->where('subject', $subject)->first(); 457 | 458 | return $jwt->public_key; 459 | } 460 | 461 | /** 462 | * @param mixed $client_id 463 | * @return bool|null 464 | */ 465 | public function getClientScope($client_id) 466 | { 467 | if (!$clientDetails = $this->getClientDetails($client_id)) { 468 | return false; 469 | } 470 | 471 | if (isset($clientDetails['scope'])) { 472 | return $clientDetails['scope']; 473 | } 474 | 475 | return null; 476 | } 477 | 478 | /** 479 | * @param mixed $client_id 480 | * @param $subject 481 | * @param $audience 482 | * @param $expires 483 | * @param $jti 484 | * @return array|null 485 | */ 486 | public function getJti($client_id, $subject, $audience, $expires, $jti) 487 | { 488 | /*$jti = Models\Jti::where('issuer', $client_id) 489 | ->where('subject', $subject) 490 | ->where('audience', $audience) 491 | ->where('expires', $expires) 492 | ->where('jti', $jti) 493 | ->first(); 494 | 495 | if ($jti) { 496 | return array( 497 | 'issuer' => $jti['issuer'], 498 | 'subject' => $jti['subject'], 499 | 'audience' => $jti['audience'], 500 | 'expires' => $jti['expires'], 501 | 'jti' => $jti['jti'], 502 | ); 503 | }*/ 504 | 505 | return null; 506 | } 507 | 508 | /** 509 | * @param mixed $client_id 510 | * @param $subject 511 | * @param $audience 512 | * @param $expires 513 | * @param $jti 514 | * @return bool 515 | */ 516 | public function setJti($client_id, $subject, $audience, $expires, $jti) 517 | { 518 | /*return Models\Jti::create([ 519 | 'issuer' => $client_id, 520 | 'subject' => $subject, 521 | 'audience' => $audience, 522 | 'expires' => $expires, 523 | 'jti' => $jti, 524 | ]);*/ 525 | } 526 | 527 | /* OAuth2\Storage\PublicKeyInterface */ 528 | 529 | /** 530 | * @param mixed $client_id 531 | * @return mixed 532 | */ 533 | public function getPublicKey($client_id = null) 534 | { 535 | /*$result = Models\PublicKey::where('client_id', $client_id)->first(); 536 | 537 | if ($result) { 538 | return $result['public_key']; 539 | }*/ 540 | } 541 | 542 | /** 543 | * @param mixed $client_id 544 | * @return mixed 545 | */ 546 | public function getPrivateKey($client_id = null) 547 | { 548 | /*$result = Models\PublicKey::where('client_id', $client_id)->first(); 549 | 550 | if ($result) { 551 | return $result['private_key']; 552 | }*/ 553 | } 554 | 555 | /** 556 | * @param mixed $client_id 557 | * @return string 558 | */ 559 | public function getEncryptionAlgorithm($client_id = null) 560 | { 561 | /*$result = Models\PublicKey::where('client_id', $client_id)->first(); 562 | 563 | if ($result) { 564 | return $result['encryption_algorithm']; 565 | }*/ 566 | 567 | return 'RS256'; 568 | } 569 | 570 | } 571 | -------------------------------------------------------------------------------- /js/dist/forum.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"forum.js","mappings":"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","sources":["webpack://@foskym/flarum-oauth-center/webpack/bootstrap","webpack://@foskym/flarum-oauth-center/webpack/runtime/compat get default export","webpack://@foskym/flarum-oauth-center/webpack/runtime/define property getters","webpack://@foskym/flarum-oauth-center/webpack/runtime/hasOwnProperty shorthand","webpack://@foskym/flarum-oauth-center/webpack/runtime/make namespace object","webpack://@foskym/flarum-oauth-center/external root \"flarum.core.compat['common/extenders']\"","webpack://@foskym/flarum-oauth-center/./node_modules/@babel/runtime/helpers/esm/setPrototypeOf.js","webpack://@foskym/flarum-oauth-center/./node_modules/@babel/runtime/helpers/esm/inheritsLoose.js","webpack://@foskym/flarum-oauth-center/external root \"flarum.core.compat['common/Model']\"","webpack://@foskym/flarum-oauth-center/./src/common/models/Client.js","webpack://@foskym/flarum-oauth-center/./src/common/models/Scope.js","webpack://@foskym/flarum-oauth-center/./src/common/models/Record.js","webpack://@foskym/flarum-oauth-center/./src/common/models/Token.js","webpack://@foskym/flarum-oauth-center/./src/common/extend.js","webpack://@foskym/flarum-oauth-center/external root \"flarum.core.compat['forum/app']\"","webpack://@foskym/flarum-oauth-center/external root \"flarum.core.compat['common/extend']\"","webpack://@foskym/flarum-oauth-center/external root \"flarum.core.compat['forum/components/UserPage']\"","webpack://@foskym/flarum-oauth-center/external root \"flarum.core.compat['common/components/LinkButton']\"","webpack://@foskym/flarum-oauth-center/./node_modules/@babel/runtime/helpers/esm/extends.js","webpack://@foskym/flarum-oauth-center/external root \"flarum.core.compat['common/components/Page']\"","webpack://@foskym/flarum-oauth-center/external root \"flarum.core.compat['forum/components/IndexPage']\"","webpack://@foskym/flarum-oauth-center/external root \"flarum.core.compat['forum/components/LogInModal']\"","webpack://@foskym/flarum-oauth-center/external root \"flarum.core.compat['common/utils/extractText']\"","webpack://@foskym/flarum-oauth-center/external root \"flarum.core.compat['common/components/Tooltip']\"","webpack://@foskym/flarum-oauth-center/external root \"flarum.core.compat['common/components/Button']\"","webpack://@foskym/flarum-oauth-center/external root \"flarum.core.compat['common/components/LoadingIndicator']\"","webpack://@foskym/flarum-oauth-center/external root \"flarum.core.compat['common/helpers/avatar']\"","webpack://@foskym/flarum-oauth-center/external root \"flarum.core.compat['common/Component']\"","webpack://@foskym/flarum-oauth-center/./src/forum/components/ScopeComponent.js","webpack://@foskym/flarum-oauth-center/./src/forum/components/oauth/AuthorizePage.js","webpack://@foskym/flarum-oauth-center/external root \"flarum.core.compat['common/components/Placeholder']\"","webpack://@foskym/flarum-oauth-center/./src/forum/components/user/AuthorizedPage.js","webpack://@foskym/flarum-oauth-center/./src/forum/index.js"],"sourcesContent":["// The require scope\nvar __webpack_require__ = {};\n\n","// getDefaultExport function for compatibility with non-harmony modules\n__webpack_require__.n = (module) => {\n\tvar getter = module && module.__esModule ?\n\t\t() => (module['default']) :\n\t\t() => (module);\n\t__webpack_require__.d(getter, { a: getter });\n\treturn getter;\n};","// define getter functions for harmony exports\n__webpack_require__.d = (exports, definition) => {\n\tfor(var key in definition) {\n\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n\t\t}\n\t}\n};","__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))","// define __esModule on exports\n__webpack_require__.r = (exports) => {\n\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n\t}\n\tObject.defineProperty(exports, '__esModule', { value: true });\n};","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/extenders'];","export default function _setPrototypeOf(o, p) {\n _setPrototypeOf = Object.setPrototypeOf ? Object.setPrototypeOf.bind() : function _setPrototypeOf(o, p) {\n o.__proto__ = p;\n return o;\n };\n return _setPrototypeOf(o, p);\n}","import setPrototypeOf from \"./setPrototypeOf.js\";\nexport default function _inheritsLoose(subClass, superClass) {\n subClass.prototype = Object.create(superClass.prototype);\n subClass.prototype.constructor = subClass;\n setPrototypeOf(subClass, superClass);\n}","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/Model'];","import Model from 'flarum/common/Model';\n\nexport default class Client extends Model {\n client_id = Model.attribute('client_id');\n client_secret = Model.attribute('client_secret');\n redirect_uri = Model.attribute('redirect_uri');\n grant_types = Model.attribute('grant_types');\n scope = Model.attribute('scope');\n user_id = Model.attribute('user_id');\n client_name = Model.attribute('client_name');\n client_icon = Model.attribute('client_icon');\n client_desc = Model.attribute('client_desc');\n client_home = Model.attribute('client_home');\n}\n","import Model from 'flarum/common/Model';\n\nexport default class Scope extends Model {\n scope = Model.attribute('scope');\n resource_path = Model.attribute('resource_path');\n method = Model.attribute('method');\n visible_fields = Model.attribute('visible_fields');\n is_default = Model.attribute('is_default');\n scope_name = Model.attribute('scope_name');\n scope_icon = Model.attribute('scope_icon');\n scope_desc = Model.attribute('scope_desc');\n}\n","import Model from 'flarum/common/Model';\n\nexport default class Record extends Model {\n client = Model.hasOne('client');\n user_id = Model.attribute('user_id');\n authorized_at = Model.attribute('authorized_at', Model.transformDate);\n}\n","import Model from 'flarum/common/Model';\n\nexport default class Token extends Model {\n access_token = Model.attribute('access_token');\n client_id = Model.attribute('client_id');\n user_id = Model.attribute('user_id');\n expires = Model.attribute('expires', Model.transformDate);\n scope = Model.attribute('scope');\n}\n","import Extend from 'flarum/common/extenders';\nimport Client from \"./models/Client\";\nimport Scope from \"./models/Scope\";\nimport Record from \"./models/Record\";\nimport Token from \"./models/Token\";\n\nexport default [\n new Extend.Store()\n .add('oauth-clients', Client)\n .add('oauth-scopes', Scope)\n .add('oauth-records', Record)\n .add('oauth-tokens', Token)\n];\n","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['forum/app'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/extend'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['forum/components/UserPage'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/components/LinkButton'];","export default function _extends() {\n _extends = Object.assign ? Object.assign.bind() : function (target) {\n for (var i = 1; i < arguments.length; i++) {\n var source = arguments[i];\n for (var key in source) {\n if (Object.prototype.hasOwnProperty.call(source, key)) {\n target[key] = source[key];\n }\n }\n }\n return target;\n };\n return _extends.apply(this, arguments);\n}","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/components/Page'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['forum/components/IndexPage'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['forum/components/LogInModal'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/utils/extractText'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/components/Tooltip'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/components/Button'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/components/LoadingIndicator'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/helpers/avatar'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/Component'];","import app from 'flarum/forum/app';\nimport Component from 'flarum/common/Component';\nimport Tooltip from 'flarum/common/components/Tooltip';\n\nexport default class ScopeComponent extends Component {\n view() {\n const { scope, client } = this.attrs;\n return (\n
\n
\n {\n (scope.scope_icon().indexOf('fa-') > -1) ?\n :\n \n }\n
\n
\n
\n {scope.scope_name()}\n
\n \n {\n scope.scope_desc()\n .replace('{client_name}', client.client_name())\n .replace('{user}', app.session.user.attribute('displayName'))\n }\n \n
\n
\n );\n }\n}\n","import app from 'flarum/forum/app';\nimport {extend} from 'flarum/common/extend';\nimport Page from 'flarum/common/components/Page';\nimport IndexPage from 'flarum/forum/components/IndexPage';\nimport LogInModal from 'flarum/forum/components/LogInModal';\nimport extractText from 'flarum/common/utils/extractText';\nimport Tooltip from 'flarum/common/components/Tooltip';\nimport Button from 'flarum/common/components/Button';\nimport LoadingIndicator from 'flarum/common/components/LoadingIndicator';\nimport avatar from 'flarum/common/helpers/avatar';\n\nimport ScopeComponent from '../ScopeComponent';\n\nexport default class AuthorizePage extends IndexPage {\n params = [];\n client = null;\n scopes = null;\n client_scope = [];\n loading = true;\n is_authorized = false;\n submit_loading = false;\n display_mode = 'box';\n\n oninit(vnode) {\n super.oninit(vnode);\n if (!app.session.user) {\n setTimeout(() => app.modal.show(LogInModal), 500);\n }\n\n this.display_mode = app.forum.attribute('foskym-oauth-center.display_mode') || 'box';\n\n const params = m.route.param();\n\n if (params.client_id == null || params.response_type == null || params.redirect_uri == null) {\n m.route.set('/');\n return;\n }\n\n this.params = params;\n\n Promise.all([\n app.store.find('oauth-clients', params.client_id),\n app.store.find('oauth-scopes')\n ]).then(([client, scopes]) => {\n if (!client) {\n m.route.set('/');\n return;\n }\n\n this.client = client;\n this.scopes = scopes;\n\n let uris = this.client.redirect_uri().split(' ');\n\n if (app.forum.attribute('foskym-oauth-center.require_exact_redirect_uri') && !uris.includes(params.redirect_uri)) {\n m.route.set('/');\n return;\n }\n\n if (!app.forum.attribute('foskym-oauth-center.allow_implicit') && params.response_type === 'token') {\n m.route.set('/');\n return;\n }\n\n if (app.forum.attribute('foskym-oauth-center.enforce_state') && params.state == null) {\n m.route.set('/');\n return;\n }\n\n let scopes_temp = params.scope ? params.scope.split(' ') : (this.client.scope() || '').split(' ');\n\n let default_scopes = this.scopes.filter(scope => scope.is_default() === 1).map(scope => scope.scope());\n\n this.client_scope = scopes_temp.filter((scope, index) => scopes_temp.indexOf(scope) === index);\n this.client_scope = this.client_scope.concat(default_scopes).filter(scope => scope !== '');\n\n this.loading = false;\n m.redraw();\n });\n }\n\n setTitle() {\n app.setTitle(extractText(app.translator.trans('foskym-oauth-center.forum.page.title.authorize')));\n app.setTitleCount(0);\n }\n\n view() {\n if (!this.client || this.loading) {\n return ;\n }\n return (\n
\n
\n
\n
\n
\n

{app.forum.attribute('title')}

\n

\n {app.translator.trans('foskym-oauth-center.forum.authorize.access')} \n {this.client.client_name()}\n \n

\n\n
\n
\n\n
\n {avatar(app.session.user, {className: 'oauth-avatar'})}\n
\n {app.session.user.username()}\n {app.session.user.displayName()}\n
\n
\n\n
\n \n \"favicon\"/\n \n \n \n \"client_icon\"/\n \n {this.client.client_name()}\n
\n
\n

{app.translator.trans('foskym-oauth-center.forum.authorize.require_these_scopes')}

\n {\n this.client_scope\n .filter(scope => scope)\n .map(scope => {\n let scope_info = this.scopes.find(s => s.scope() === scope);\n return scope_info && ;\n })\n }\n
\n
\n {Object.keys(this.params).map(key => (\n \n ))}\n \n
\n \n \n
\n
\n
\n
\n
\n
\n
\n );\n }\n deny(e) {\n this.is_authorized = false;\n }\n agree(e) {\n this.is_authorized = true;\n }\n\n onsubmit(e) {\n e.preventDefault();\n this.submit_loading = true;\n if (app.forum.attribute('foskym-oauth-center.authorization_method_fetch')) {\n app.request({\n method: 'POST',\n url: '/oauth/authorize/fetch',\n body: {\n ...this.params,\n is_authorized: this.is_authorized,\n }\n }).then((params) => {\n window.location.href = params.location;\n });\n } else {\n e.target.submit();\n }\n }\n}\n","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/components/Placeholder'];","import app from 'flarum/forum/app';\nimport UserPage from 'flarum/forum/components/UserPage';\nimport LoadingIndicator from 'flarum/common/components/LoadingIndicator';\nimport Placeholder from 'flarum/common/components/Placeholder';\nimport Button from 'flarum/common/components/Button';\n\nexport default class AuthorizedPage extends UserPage {\n records = [];\n loading = true;\n nomore = false;\n page = 0;\n oninit(vnode) {\n super.oninit(vnode);\n this.loadUser(m.route.param('username'));\n this.loadRecords();\n }\n loadRecords() {\n app.store.find('oauth-records', { page: this.page }).then((records) => {\n this.records = this.records.concat(records);\n this.loading = false;\n if (records.length < 10) {\n this.nomore = true;\n }\n m.redraw();\n });\n }\n loadMore() {\n this.loadRecords((this.page += 1));\n }\n content() {\n if (this.records.length === 0) {\n return ;\n }\n\n let allow_delete = app.forum.attribute('foskym-oauth-center.allow_delete_records');\n console.log(allow_delete);\n\n return (\n
\n {allow_delete && (\n {\n window.confirm(app.translator.trans('foskym-oauth-center.forum.authorized.delete_all_confirm')) &&\n app\n .request({\n url: app.forum.attribute('apiUrl') + '/oauth-records/user',\n method: 'DELETE',\n })\n .then(() => {\n this.records = [];\n this.nomore = false;\n this.loadMore();\n });\n }}\n >\n {app.translator.trans('foskym-oauth-center.forum.authorized.delete_all_button')}\n \n )}\n\n
    \n {this.records.map((record) => (\n
  • \n
    \n
    \n \"client_icon\"\n
    \n

    \n \n {record.attribute('client').client_name}\n \n

    \n

    {record.attribute('client').client_desc}

    \n
    \n
    \n
    \n \n\n {allow_delete && (\n {\n record.delete().then(() => {\n this.records = this.records.filter((r) => r !== record);\n m.redraw();\n });\n }}\n >\n {app.translator.trans('foskym-oauth-center.forum.authorized.delete_button')}\n \n )}\n
    \n
    \n
    \n
  • \n ))}\n
\n\n {this.loading && }\n\n {!this.loading && !this.nomore && (\n
\n \n
\n )}\n\n {this.nomore && }\n
\n );\n }\n}\n","import app from 'flarum/forum/app';\nimport {extend} from 'flarum/common/extend';\nimport UserPage from 'flarum/forum/components/UserPage';\nimport LinkButton from 'flarum/common/components/LinkButton';\nimport AuthorizePage from \"./components/oauth/AuthorizePage\";\nimport AuthorizedPage from \"./components/user/AuthorizedPage\";\napp.initializers.add('foskym/flarum-oauth-center', () => {\n app.routes['oauth.authorize'] = {\n path: '/oauth/authorize',\n component: AuthorizePage\n };\n\n app.routes['user.authorized'] = {\n path: '/u/:username/authorized',\n component: AuthorizedPage\n };\n extend(UserPage.prototype, 'navItems', function (items) {\n if (app.session.user && app.session.user.id() === this.user.id()) {\n items.add(\n 'authorized',\n LinkButton.component(\n {\n href: app.route('user.authorized', { username: this.user.username() }),\n icon: 'fas fa-user-friends',\n },\n [\n app.translator.trans('foskym-oauth-center.forum.page.label.authorized'),\n ]\n ),\n -110\n );\n }\n });\n});\n"],"names":["__webpack_require__","module","getter","__esModule","d","a","exports","definition","key","o","Object","defineProperty","enumerable","get","obj","prop","prototype","hasOwnProperty","call","Symbol","toStringTag","value","flarum","core","compat","_setPrototypeOf","p","setPrototypeOf","bind","__proto__","_inheritsLoose","subClass","superClass","create","constructor","Client","_Model","_this","_len","arguments","length","args","Array","_key","apply","concat","client_id","Model","client_secret","redirect_uri","grant_types","scope","user_id","client_name","client_icon","client_desc","client_home","Scope","resource_path","method","visible_fields","is_default","scope_name","scope_icon","scope_desc","Record","client","authorized_at","Token","access_token","expires","Extend","add","_extends","assign","target","i","source","this","ScopeComponent","_Component","view","_this$attrs","attrs","m","class","indexOf","style","src","replace","app","user","attribute","Component","AuthorizePage","_IndexPage","params","scopes","client_scope","loading","is_authorized","submit_loading","display_mode","_proto","oninit","vnode","_this2","setTimeout","show","LogInModal","route","param","response_type","Promise","all","find","then","_ref","uris","split","includes","state","set","scopes_temp","default_scopes","filter","map","index","redraw","setTitle","extractText","trans","_this3","LoadingIndicator","className","Tooltip","text","position","href","avatar","username","displayName","alt","scope_info","s","id","action","onsubmit","keys","type","name","Button","onclick","deny","agree","e","preventDefault","url","body","window","location","submit","IndexPage","AuthorizedPage","_UserPage","records","nomore","page","loadUser","loadRecords","loadMore","content","Placeholder","allow_delete","console","log","confirm","record","toLocaleString","r","display","disabled","UserPage","path","component","extend","items","LinkButton","icon"],"sourceRoot":""} --------------------------------------------------------------------------------