diff --git a/extend.php b/extend.php index c8f1edd..b4680d5 100644 --- a/extend.php +++ b/extend.php @@ -12,7 +12,10 @@ namespace FoskyM\OAuthCenter; use Flarum\Extend; +use Flarum\Http\Middleware\AuthenticateWithHeader; +use Flarum\Http\Middleware\CheckCsrfToken; use FoskyM\OAuthCenter\Middlewares\ResourceScopeMiddleware; +use FoskyM\OAuthCenter\Middlewares\UnsetCsrfMiddleware; return [ (new Extend\Frontend('forum')) @@ -26,9 +29,30 @@ return [ new Extend\Locales(__DIR__.'/locale'), (new Extend\Routes('forum')) - ->post('/oauth/authorize', 'oauth.authorize.post', Controllers\AuthorizeController::class), - (new Extend\Routes('api')) - ->get('/oauth/clients', 'oauth.clients.list', Api\Controller\ListClientController::class), + ->post('/oauth/authorize', 'oauth.authorize.post', Controllers\AuthorizeController::class) + ->post('/oauth/token', 'oauth.token', Controllers\TokenController::class), - (new Extend\Middleware('api'))->add(ResourceScopeMiddleware::class), + (new Extend\Routes('api')) + ->get('/oauth-clients', 'oauth.clients.list', Api\Controller\ListClientController::class) + ->post('/oauth-clients', 'oauth.clients.create', Api\Controller\CreateClientController::class) + ->get('/oauth-clients/{client_id}', 'oauth.clients.show', Api\Controller\ShowClientController::class) + ->patch('/oauth-clients/{id}', 'oauth.clients.update', Api\Controller\UpdateClientController::class) + ->delete('/oauth-clients/{id}', 'oauth.clients.delete', Api\Controller\DeleteClientController::class) + + ->get('/oauth-scopes', 'oauth.scopes.list', Api\Controller\ListScopeController::class) + ->post('/oauth-scopes', 'oauth.scopes.create', Api\Controller\CreateScopeController::class) + ->patch('/oauth-scopes/{id}', 'oauth.scopes.update', Api\Controller\UpdateScopeController::class) + ->delete('/oauth-scopes/{id}', 'oauth.scopes.delete', Api\Controller\DeleteScopeController::class) + + ->get('/user', 'user.show', Controllers\ApiUserController::class), + + (new Extend\Settings) + ->serializeToForum('foskym-oauth-center.allow_implicit', 'foskym-oauth-center.allow_implicit', 'boolval') + ->serializeToForum('foskym-oauth-center.enforce_state', 'foskym-oauth-center.enforce_state', 'boolval') + ->serializeToForum('foskym-oauth-center.require_exact_redirect_uri', 'foskym-oauth-center.require_exact_redirect_uri', 'boolval'), + + (new Extend\Middleware('api')) + ->insertAfter(AuthenticateWithHeader::class, ResourceScopeMiddleware::class), + (new Extend\Middleware('forum')) + ->insertBefore(CheckCsrfToken::class, UnsetCsrfMiddleware::class), ]; diff --git a/js/dist/admin.js b/js/dist/admin.js index 47a216f..d45ab95 100644 Binary files a/js/dist/admin.js and b/js/dist/admin.js differ diff --git a/js/dist/admin.js.map b/js/dist/admin.js.map index 5f1a3e8..21de8ca 100644 Binary files a/js/dist/admin.js.map and b/js/dist/admin.js.map differ diff --git a/js/dist/forum.js b/js/dist/forum.js index 3003770..4c8eaf1 100644 Binary files a/js/dist/forum.js and b/js/dist/forum.js differ diff --git a/js/dist/forum.js.map b/js/dist/forum.js.map index 8eac3b2..c314cbb 100644 Binary files a/js/dist/forum.js.map and b/js/dist/forum.js.map differ diff --git a/js/src/admin/pages/ClientsPage.js b/js/src/admin/pages/ClientsPage.js index fd464c3..b05e0af 100644 --- a/js/src/admin/pages/ClientsPage.js +++ b/js/src/admin/pages/ClientsPage.js @@ -1,22 +1,100 @@ import app from 'flarum/admin/app'; import Page from 'flarum/common/components/Page'; - +import Button from 'flarum/common/components/Button'; export default class ClientsPage extends Page { - settingName = 'collapsible-posts.reasons'; translationPrefix = 'foskym-oauth-center.admin.clients.'; + clients = []; oninit(vnode) { super.oninit(vnode); - app.store.find('oauth/clients').then(() => { + this.fields = [ + 'client_id', + 'client_secret', + 'redirect_uri', + 'grant_types', + 'scope', + 'client_name', + 'client_desc', + 'client_icon', + 'client_home' + ]; + + app.store.find('oauth-clients').then(r => { + this.clients = r; + this.fields.map(key => console.log(this.clients[0][key])) m.redraw(); }); } view() { return ( -
-

Clients Page

+
+ { + m('.Form-group', [ + m('table', [ + m('thead', m('tr', [ + this.fields.map(key => m('th', app.translator.trans(this.translationPrefix + key))), + m('th'), + ])), + m('tbody', [ + this.clients.map((client, index) => m('tr', [ + this.fields.map(key => + m('td', m('input.FormControl', { + type: 'text', + value: client[key]() || '', + onchange: (event) => { + this.saveClientInfo(index, key, event.target.value); + }, + })) + ), + m('td', Button.component({ + className: 'Button Button--icon', + icon: 'fas fa-times', + onclick: () => { + this.clients[index].delete(); + this.clients.splice(index, 1); + + }, + })), + ])), + m('tr', m('td', { + colspan: 9, + }, Button.component({ + className: 'Button Button--block', + onclick: () => { + const client = app.store.createRecord('oauth-clients'); + const client_id = this.randomString(32); + const client_secret = this.randomString(32); + client.save({ + client_id: client_id, + client_secret: client_secret, + }).then(this.clients.push(client)); + }, + }, app.translator.trans(this.translationPrefix + 'add_button')))), + ]), + ]), + ]) + }
); } + + randomString(len) { + len = len || 32; + let $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + let maxPos = $chars.length; + let pwd = ''; + for (let i = 0; i < len; i++) { + //0~32的整数 + pwd += $chars.charAt(Math.floor(Math.random() * (maxPos + 1))); + } + return pwd; + } + + saveClientInfo(index, key, value) { + console.log(index, key, value); + this.clients[index].save({ + [key]: value, + }); + } } diff --git a/js/src/admin/pages/ScopesPage.js b/js/src/admin/pages/ScopesPage.js index e830f7d..2fb36d0 100644 --- a/js/src/admin/pages/ScopesPage.js +++ b/js/src/admin/pages/ScopesPage.js @@ -1,11 +1,118 @@ +import app from 'flarum/admin/app'; import Page from 'flarum/common/components/Page'; +import Button from 'flarum/common/components/Button'; +import Select from 'flarum/common/components/Select'; +import Checkbox from 'flarum/common/components/Checkbox'; export default class ScopesPage extends Page { - view() { - return ( -
-

Scopes Page

-
- ); + translationPrefix = 'foskym-oauth-center.admin.scopes.'; + scopes = []; + + oninit(vnode) { + super.oninit(vnode); + + this.fields = [ + 'scope', + 'resource_path', + 'method', + 'is_default', + 'scope_name', + 'scope_icon', + 'scope_desc' + ]; + + app.store.find('oauth-scopes').then(r => { + this.scopes = r; + this.fields.map(key => console.log(this.scopes[0][key])) + m.redraw(); + }); + } + + view() { + return ( +
+ { + m('.Form-group', [ + m('table', [ + m('thead', m('tr', [ + this.fields.map(key => m('th', app.translator.trans(this.translationPrefix + key))), + m('th'), + ])), + m('tbody', [ + this.scopes.map((scope, index) => m('tr', [ + this.fields.map(key => + m('td', key === 'method' ? Select.component({ + options: { + 'GET': 'GET', + 'POST': 'POST', + 'PUT': 'PUT', + 'DELETE': 'DELETE', + 'PATCH': 'PATCH', + }, + value: scope[key]() || 'GET', + disabled: scope.resource_path() === '/api/user' && key === 'method', + onchange: (value) => { + this.saveScopeInfo(index, key, value); + }, + }) : key === 'is_default' ? Checkbox.component({ + state: scope[key]() === 1 || false, + disabled: scope.resource_path() === '/api/user' && key === 'is_default', + onchange: (checked) => { + this.scopes[index].is_default((this.scopes[index].is_default() + 1) % 2) + this.saveScopeInfo(index, key, checked ? 1 : 0); + }, + }) : m('input.FormControl', { + type: 'text', + value: scope[key]() || '', + disabled: scope.resource_path() === '/api/user' && key === 'resource_path', + onchange: (event) => { + this.saveScopeInfo(index, key, event.target.value); + }, + })) + ), + (scope.resource_path() !== '/api/user' && m('td', Button.component({ + className: 'Button Button--icon', + icon: 'fas fa-times', + onclick: () => { + this.scopes[index].delete(); + this.scopes.splice(index, 1); + }, + }))), + ])), + m('tr', m('td', { + colspan: 7, + }, Button.component({ + className: 'Button Button--block', + onclick: () => { + const scope = app.store.createRecord('oauth-scopes'); + scope.save({ + 'scope': 'Scope.' + this.randomString(8), + 'resource_path': '/api/' + this.randomString(4), + 'method': 'GET', + }).then(this.scopes.push(scope)); + }, + }, app.translator.trans(this.translationPrefix + 'add_button')))), + ]), + ]), + ]) + } +
+ ); + } + randomString(len) { + len = len || 8; + let $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + let maxPos = $chars.length; + let str = ''; + for (let i = 0; i < len; i++) { + //0~32的整数 + str += $chars.charAt(Math.floor(Math.random() * (maxPos + 1))); } + return str; + } + saveScopeInfo(index, key, value) { + this.scopes[index].save({ + [key]: value, + }); + } } diff --git a/js/src/common/extend.js b/js/src/common/extend.js index 54f41cd..8e49d35 100644 --- a/js/src/common/extend.js +++ b/js/src/common/extend.js @@ -1,7 +1,9 @@ import Extend from 'flarum/common/extenders'; import Client from "./models/Client"; +import Scope from "./models/Scope"; export default [ new Extend.Store() - .add('oauth-clients', Client), + .add('oauth-clients', Client) + .add('oauth-scopes', Scope), ]; diff --git a/js/src/common/models/Scope.js b/js/src/common/models/Scope.js new file mode 100644 index 0000000..2b89440 --- /dev/null +++ b/js/src/common/models/Scope.js @@ -0,0 +1,11 @@ +import Model from 'flarum/common/Model'; + +export default class Scope extends Model { + scope = Model.attribute('scope'); + resource_path = Model.attribute('resource_path'); + method = Model.attribute('method'); + is_default = Model.attribute('is_default'); + scope_name = Model.attribute('scope_name'); + scope_icon = Model.attribute('scope_icon'); + scope_desc = Model.attribute('scope_desc'); +} diff --git a/js/src/forum/components/oauth/AuthorizePage.js b/js/src/forum/components/oauth/AuthorizePage.js index e853bd6..6d31f91 100644 --- a/js/src/forum/components/oauth/AuthorizePage.js +++ b/js/src/forum/components/oauth/AuthorizePage.js @@ -4,8 +4,18 @@ import Page from 'flarum/common/components/Page'; import IndexPage from 'flarum/forum/components/IndexPage'; import LogInModal from 'flarum/forum/components/LogInModal'; import extractText from 'flarum/common/utils/extractText'; +import Tooltip from 'flarum/common/components/Tooltip'; +import Button from 'flarum/common/components/Button'; export default class AuthorizePage extends IndexPage { + params = []; + client = null; + scopes = null; + client_scope = []; + loading = true; + is_authorized = false; + submit_loading = false; + oninit(vnode) { super.oninit(vnode); if (!app.session.user) { @@ -13,18 +23,205 @@ export default class AuthorizePage extends IndexPage { } const params = m.route.param(); + + if (params.client_id == null || params.response_type == null || params.redirect_uri == null) { + m.route.set('/'); + } else { + this.params = params; + app.store.find('oauth-clients', params.client_id).then(client => { + if (client.length === 0) { + m.route.set('/'); + } else { + this.client = client[0]; + let uris = null; + if (this.client.redirect_uri().indexOf(' ') > -1) { + uris = this.client.redirect_uri().split(' '); + } else { + uris = [this.client.redirect_uri()]; + } + + if (app.forum.attribute('foskym-oauth-center.require_exact_redirect_uri') && uris.indexOf(params.redirect_uri) == -1) { + m.route.set('/'); + } + if (app.forum.attribute('foskym-oauth-center.allow_implicit') && params.response_type == 'token') { + m.route.set('/'); + } + if (app.forum.attribute('foskym-oauth-center.enforce_state') && params.enforce_state == null) { + m.route.set('/'); + } + + app.store.find('oauth-scopes').then((scopes) => { + this.scopes = scopes + let scope = params.scope; + + if (params.scope == null) { + scope = this.client.scope(); + } + + let scopes_temp = []; + if (scope == null) { + scopes_temp = []; + } else if (scope.indexOf(' ') > -1) { + scopes_temp = scope.split(' '); + } else { + scopes_temp = [scope]; + } + + let default_scopes = []; + this.scopes.map(scope => { + let index = scopes_temp.indexOf(scope.scope()); + if (index > -1) { + scopes_temp[index] = scope; + } else { + scopes_temp.slice(index, 1); + } + if (scope.is_default() === 1) { + default_scopes.push(scope); + } + }); + + scopes_temp = scopes_temp.concat(default_scopes); + + this.client_scope = scopes_temp.filter((scope, index) => scopes_temp.indexOf(scope) === index); + console.log(this.client_scope); + this.loading = false; + m.redraw(); + }); + + + } + }); + } } setTitle() { app.setTitle(extractText(app.translator.trans('foskym-oauth-center.forum.page.title.authorize'))); app.setTitleCount(0); } + view() { + if (!this.client) { + return ''; + } return ( -
+ !this.loading &&
+
+
+
+

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

+

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

+ +
+
+ +
+ + + + + +
+
+ { + this.client_scope.length > 0 && this.client_scope.map(scope => { + let scope_info = null; + this.scopes.map(s => { + if (s.scope() === scope.scope()) { + scope_info = s; + } + }); + if (scope_info == null) { + return ''; + } + return ( +
+
+ { + (scope_info.scope_icon().indexOf('fa-') > -1) ? + : + + } +
+
+
+ {scope_info.scope_name()} +
+ + { + scope_info.scope_desc() + .replace('{client_name}', this.client.client_name()) + .replace('{user}', app.session.user.attribute('displayName')) + } + +
+
+ ); + }) + } +
+
+ {/* + + + + */} + +
+ + +
+
+ + +
+
+
); } + deny(e) { + this.is_authorized = false; + } + agree(e) { + this.is_authorized = true; + } + + onsubmit(e) { + e.preventDefault(); + this.submit_loading = true; + app.request({ + method: 'POST', + url: '/oauth/authorize', + body: { + response_type: this.params.response_type, + client_id: this.params.client_id, + redirect_uri: this.params.redirect_uri, + state: this.params.state, + scope: this.params.scope, + is_authorized: this.is_authorized, + } + }).then((params) => { + let arr = [] + for (let k in params) { + arr.push(`${k}=${params[k]}`) + } + let url = `${this.params.redirect_uri }?${arr.join('&')}`; + window.location.href = url; + }); + + // Some form handling logic here + } } diff --git a/js/src/forum/index.js b/js/src/forum/index.js index 3ba3f21..c6ee2f4 100644 --- a/js/src/forum/index.js +++ b/js/src/forum/index.js @@ -16,20 +16,19 @@ app.initializers.add('foskym/flarum-oauth-center', () => { }; extend(UserPage.prototype, 'navItems', function (items) { if (app.session.user && app.session.user.id() === this.user.id()) { - items.add( - 'authorized', - LinkButton.component( - { - href: app.route('user.authorized', { username: this.user.username() }), - icon: 'fas fa-user-friends', - }, - [ - app.translator.trans('foskym-oauth-center.forum.page.label.authorized'), - // this.user.moderatorNoteCount() > 0 ? {this.user.moderatorNoteCount()} : '', - ] - ), - -110 - ); + // items.add( + // 'authorized', + // LinkButton.component( + // { + // href: app.route('user.authorized', { username: this.user.username() }), + // icon: 'fas fa-user-friends', + // }, + // [ + // app.translator.trans('foskym-oauth-center.forum.page.label.authorized'), + // ] + // ), + // -110 + // ); } }); }); diff --git a/less/admin.less b/less/admin.less index 9795d1f..8add5d6 100644 --- a/less/admin.less +++ b/less/admin.less @@ -24,6 +24,39 @@ } } } + + .OAuthCenterPage-container { + max-width: 100% !important; + } + + .OAuthCenter-clientsPage, .OAuthCenter-scopesPage { + table { + width: 100%; + + td, th { + padding: 3px 5px; + } + + th { + text-align: left; + } + } + + .Checkbox { + padding: 0 10px; + } + + .FormControl { + background: @body-bg; + border-color: @control-bg; + + // We set the same as Flarum default, but with more specificity + &:focus, + &.focus { + border-color: @primary-color; + } + } + } } @media (min-width: 992px) { .OAuthCenter { diff --git a/less/forum.less b/less/forum.less index e69de29..8a23319 100644 --- a/less/forum.less +++ b/less/forum.less @@ -0,0 +1 @@ +@import url('./forum/oauth'); diff --git a/less/forum/oauth.less b/less/forum/oauth.less new file mode 100644 index 0000000..c85eea5 --- /dev/null +++ b/less/forum/oauth.less @@ -0,0 +1,226 @@ +.oauth-area { + display: block !important; + position: relative; + left: 0; + top: 0; + padding: 110px 0; + min-height: 100%; + box-sizing: border-box; +} + +.oauth-main { + position: relative; + width: 376px; + margin: 0 auto; + box-sizing: border-box; + box-shadow: 0px 0px 15px 0px #bdbdbd; + border-radius: 12px; +} + +.oauth-main::before { + backdrop-filter: blur(20px); + content: ''; + position: absolute; + width: 100%; + height: 100%; + left: 0; + top: 0; + background: hsla(0, 0%, 100%, .3); + border-radius: 12px; +} + +.oauth-box { + padding: 20px; + background-color: #f3f3f3; +} + +.oauth-header { + backdrop-filter: blur(0); + text-align: center; + box-shadow: 0 5px 10px -5px #d2d2d2; + border-radius: 12px 12px 0 0; +} + +.oauth-header h2 { + margin-bottom: 8px; + font-weight: 600; + font-size: 40px; + color: #000; +} + +.oauth-header p { + font-weight: 400; + font-size: 20px; + color: #333; +} + +.oauth-body { + border-radius: 0 0 12px 12px; +} + +.oauth-body .oauth-form-item { + position: relative; + margin-bottom: 15px; + clear: both; + *zoom: 1; +} + +.oauth-body .oauth-form-item:after { + content: '\20'; + clear: both; + *zoom: 1; + display: block; + height: 0; +} + +.oauth-icon { + position: absolute; + left: 4px; + top: 1px; + width: auto; + line-height: 35px; + text-align: center; + color: #999; + padding: 0 8px; + font-size: 14px; +} + +label:before { + color: #999; +} + +@media screen and (max-width: 768px) { + .oauth-area { + padding-top: 60px + } + + .oauth-main { + width: 300px + } + + .oauth-box { + padding: 10px + } + + .oauth-main::before { + backdrop-filter: none; + } + + .oauth-header { + // background-color: #fff; + } + + body { + margin: 0 + } +} + +@media screen and (max-width: 600px) { + .oauth-area { + padding-top: 0 + } + + body { + background: #f3f3f3 !important; + } + + .oauth-main { + width: 100%; + } + + .oauth-main::before { + box-shadow: none !important; + } + + .oauth-header { + box-shadow: none; + } + + .oauth-box:last-child { + box-shadow: 0 5px 10px -5px #d2d2d2; + } +} + +.oauth-top { + text-align: center; + padding-bottom: 20px; + position: relative; +} + +.oauth-top img { + width: 64px; + border-radius: 50%; + border: #4950578c solid 1px; + 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; + transition: all .3s; +} + +.oauth-top img:hover { + box-shadow: 0 2px 8px rgba(0, 0, 0, .3); +} + +.oauth-top i { + top: -24px; + position: relative; + padding-left: 10px; + padding-right: 10px; + color: #111; +} + +.oauth-scope-area { + padding-top: 10px; + padding-bottom: 10px; + overflow: auto; + max-height: 350px; + position: relative; +} + +.oauth-scope { + margin-top: 15px; +} + +.oauth-scope:first-child { + margin-top: 0; +} + +.oauth-scope, .oauth-scope-body { + overflow: hidden; + zoom: 1; +} + +.oauth-scope-body, .oauth-scope-left, .oauth-scope-right { + display: table-cell; + vertical-align: top; +} + +.oauth-scope-left, .oauth-scope > .pull-left { + padding-right: 10px; + min-width: 42px; + text-align: center; +} + +img.oauth-scope-object { + display: block; + vertical-align: middle; + border: 0; + width: 32px; + height: 32px; +} + +.oauth-scope-body { + width: 10000px; + padding-left: 8px; +} + +.oauth-scope-heading { + margin-top: 0; + font-weight: 800; + color: #382e2e; + margin-block-end: 0; +} + +.oauth-scope-body small { + font-weight: 500; + font-size: 12px; + color: #aaa; +} diff --git a/locale/zh-Hans.yml b/locale/zh-Hans.yml index 7bcb0f0..556fb1b 100644 --- a/locale/zh-Hans.yml +++ b/locale/zh-Hans.yml @@ -12,13 +12,23 @@ foskym-oauth-center: clients: client_id: 应用 ID client_secret: 应用密钥 - redirect_uri: 回调地址(多地址请用空格分割) - grant_types: 授权类型(可空) - scope: 权限(可空) - name: 应用名称(可空) - description: 应用描述(可空) - icon: 应用图标地址(可空 可使用fontawesome图标) - home: 主页地址(可空) + redirect_uri: 回调地址 + grant_types: 授权类型 + scope: 权限 + client_name: 名称 + client_desc: 描述 + client_icon: 图标 + client_home: 主页 + add_button: 添加应用 + scopes: + scope: 权限标识 + resource_path: 资源路径 + method: 请求方法 + is_default: 默认 + scope_name: 名称 + scope_icon: 图标 + scope_desc: 描述 + add_button: 添加权限 forum: page: @@ -26,3 +36,7 @@ foskym-oauth-center: authorize: 授权 label: authorized: 授权记录 + authorize: + access: 授权访问 + agree: 授权 + deny: 拒绝 diff --git a/migrations/2023_10_02_add_default_record_to_oauth_scopes_table.php b/migrations/2023_10_02_add_default_record_to_oauth_scopes_table.php new file mode 100644 index 0000000..bda0bc2 --- /dev/null +++ b/migrations/2023_10_02_add_default_record_to_oauth_scopes_table.php @@ -0,0 +1,32 @@ + function (Builder $schema) { + if (!$schema->hasTable('oauth_scopes')) { + return; + } + $schema->getConnection()->table('oauth_scopes')->insert([ + 'scope' => 'user.read', + 'resource_path' => '/api/user', + 'method' => 'GET', + 'is_default' => 1, + 'scope_name' => '获取用户信息', + 'scope_icon' => 'fas fa-user', + 'scope_desc' => '访问该用户({user})的个人信息等', + ]); + }, + 'down' => function (Builder $schema) { + + }, +]; diff --git a/src/Api/Controller/CreateClientController.php b/src/Api/Controller/CreateClientController.php new file mode 100644 index 0000000..866b73b --- /dev/null +++ b/src/Api/Controller/CreateClientController.php @@ -0,0 +1,29 @@ +assertAdmin(); + + $attributes = Arr::get($request->getParsedBody(), 'data.attributes'); + + return Client::create([ + 'client_id' => Arr::get($attributes, 'client_id'), + 'client_secret' => Arr::get($attributes, 'client_secret'), + 'user_id' => $actor->id, + ]); + } +} diff --git a/src/Api/Controller/CreateScopeController.php b/src/Api/Controller/CreateScopeController.php new file mode 100644 index 0000000..014d45d --- /dev/null +++ b/src/Api/Controller/CreateScopeController.php @@ -0,0 +1,27 @@ +assertAdmin(); + + $attributes = Arr::get($request->getParsedBody(), 'data.attributes'); + + return Scope::create([ + 'scope' => Arr::get($attributes, 'scope'), + ]); + } +} diff --git a/src/Api/Controller/DeleteClientController.php b/src/Api/Controller/DeleteClientController.php new file mode 100644 index 0000000..e585dfa --- /dev/null +++ b/src/Api/Controller/DeleteClientController.php @@ -0,0 +1,26 @@ +getQueryParams(), 'id'); + RequestUtil::getActor($request) + ->assertAdmin(); + + $client = Client::find($id); + + $client->delete(); + } +} diff --git a/src/Api/Controller/DeleteScopeController.php b/src/Api/Controller/DeleteScopeController.php new file mode 100644 index 0000000..52729d8 --- /dev/null +++ b/src/Api/Controller/DeleteScopeController.php @@ -0,0 +1,25 @@ +getQueryParams(), 'id'); + RequestUtil::getActor($request) + ->assertAdmin(); + + $scope = Scope::find($id); + + $scope->delete(); + } +} diff --git a/src/Api/Controller/ListClientController.php b/src/Api/Controller/ListClientController.php index caa3307..566fba0 100644 --- a/src/Api/Controller/ListClientController.php +++ b/src/Api/Controller/ListClientController.php @@ -16,10 +16,8 @@ class ListClientController extends AbstractListController protected function data(ServerRequestInterface $request, Document $document) { $actor = RequestUtil::getActor($request); - if (!$actor->isAdmin()) { - return []; - } + $actor->assertAdmin(); - return Client::get(); + return Client::all(); } } diff --git a/src/Api/Controller/ListScopeController.php b/src/Api/Controller/ListScopeController.php new file mode 100644 index 0000000..44344ef --- /dev/null +++ b/src/Api/Controller/ListScopeController.php @@ -0,0 +1,23 @@ +assertAdmin(); + + return Scope::all(); + } +} diff --git a/src/Api/Controller/ShowClientController.php b/src/Api/Controller/ShowClientController.php new file mode 100644 index 0000000..5a64375 --- /dev/null +++ b/src/Api/Controller/ShowClientController.php @@ -0,0 +1,26 @@ +getQueryParams(), 'client_id'); + RequestUtil::getActor($request)->assertRegistered(); + + $client = Client::where('client_id', $client_id)->get(); + + return $client; + + } +} diff --git a/src/Api/Controller/UpdateClientController.php b/src/Api/Controller/UpdateClientController.php new file mode 100644 index 0000000..e2cfee1 --- /dev/null +++ b/src/Api/Controller/UpdateClientController.php @@ -0,0 +1,37 @@ +assertAdmin(); + + $id = Arr::get($request->getQueryParams(), 'id'); + $client = Client::find($id); + + $attributes = Arr::get($request->getParsedBody(), 'data.attributes', []); + + collect(['client_id', 'client_secret', 'redirect_uri', 'grant_types', 'scope', 'client_name', 'client_desc', 'client_icon', 'client_home']) + ->each(function (string $attribute) use ($client, $attributes) { + if (($val = Arr::get($attributes, $attribute)) !== null) { + $client->$attribute = $val; + } + }); + + $client->save(); + + return $client; + } +} diff --git a/src/Api/Controller/UpdateScopeController.php b/src/Api/Controller/UpdateScopeController.php new file mode 100644 index 0000000..5f9aeb7 --- /dev/null +++ b/src/Api/Controller/UpdateScopeController.php @@ -0,0 +1,37 @@ +assertAdmin(); + + $id = Arr::get($request->getQueryParams(), 'id'); + $scope = Scope::find($id); + + $attributes = Arr::get($request->getParsedBody(), 'data.attributes', []); + + collect(['scope', 'resource_path', 'method', 'is_default', 'scope_name', 'scope_icon', 'scope_desc']) + ->each(function (string $attribute) use ($scope, $attributes) { + if (($val = Arr::get($attributes, $attribute)) !== null) { + $scope->$attribute = $val; + } + }); + + $scope->save(); + + return $scope; + } +} diff --git a/src/Api/Serializer/ClientPublicSerializer.php b/src/Api/Serializer/ClientPublicSerializer.php new file mode 100644 index 0000000..660fd63 --- /dev/null +++ b/src/Api/Serializer/ClientPublicSerializer.php @@ -0,0 +1,35 @@ + $model->id, + "client_id" => $model->client_id, + "redirect_uri" => $model->redirect_uri, + "grant_types" => $model->grant_types, + "scope" => $model->scope, + "client_name" => $model->client_name, + "client_icon" => $model->client_icon, + "client_desc" => $model->client_desc, + "client_home" => $model->client_home + ]; + } +} diff --git a/src/Api/Serializer/ScopeSerializer.php b/src/Api/Serializer/ScopeSerializer.php new file mode 100644 index 0000000..fabdbc7 --- /dev/null +++ b/src/Api/Serializer/ScopeSerializer.php @@ -0,0 +1,34 @@ + $model->id, + "scope" => $model->scope, + "resource_path" => $model->resource_path, + "method" => $model->method, + "is_default" => $model->is_default, + "scope_name" => $model->scope_name, + "scope_icon" => $model->scope_icon, + "scope_desc" => $model->scope_desc, + ]; + } +} diff --git a/src/Controllers/ApiUserController.php b/src/Controllers/ApiUserController.php new file mode 100644 index 0000000..d20fdd2 --- /dev/null +++ b/src/Controllers/ApiUserController.php @@ -0,0 +1,49 @@ +settings = $settings; + } + + public function handle(ServerRequestInterface $request): ResponseInterface + { + $actor = RequestUtil::getActor($request); + $actor = $actor->toArray(); + $data = [ + 'id' => $actor['id'], + 'username' => $actor['username'], + 'nickname' => $actor['nickname'], + 'avatar_url' => $actor['avatar_url'], + 'email' => $actor['email'], + 'is_email_confirmed' => $actor['is_email_confirmed'], + 'joined_at' => $actor['joined_at'], + 'last_seen_at' => $actor['last_seen_at'], + 'discussion_count' => $actor['discussion_count'], + 'comment_count' => $actor['comment_count'], + ]; + return new JsonResponse($data); + } +} diff --git a/src/Controllers/AuthorizeController.php b/src/Controllers/AuthorizeController.php index e210b87..b004f71 100644 --- a/src/Controllers/AuthorizeController.php +++ b/src/Controllers/AuthorizeController.php @@ -35,23 +35,24 @@ class AuthorizeController implements RequestHandlerInterface $params = $request->getParsedBody(); - $oauth = new OAuth(); + $oauth = new OAuth($this->settings); $server = $oauth->server(); $request = $oauth->request()::createFromGlobals(); $response = $oauth->response(); if (!$server->validateAuthorizeRequest($request, $response)) { - $response->send(); - die; + return new JsonResponse(json_decode($response->getResponseBody(), true)); } - $is_authorized = (Arr::get($params, 'authorized', 'no') === 'yes'); + $is_authorized = Arr::get($params, 'is_authorized', 0); $server->handleAuthorizeRequest($request, $response, $is_authorized, $actor->id); if ($is_authorized) { - // this is only here so that you get to see your code in the cURL request. Otherwise, we'd redirect back to the client - /* $code = substr($response->getHttpHeader('Location'), strpos($response->getHttpHeader('Location'), 'code=')+5, 40); - exit("SUCCESS! Authorization Code: $code");*/ + $code = substr($response->getHttpHeader('Location'), strpos($response->getHttpHeader('Location'), 'code=') + 5, 40); + return new JsonResponse([ + 'code' => $code + ]); } - $response->send(); + + return new JsonResponse(json_decode($response->getResponseBody(), true)); } } diff --git a/src/Controllers/TokenController.php b/src/Controllers/TokenController.php new file mode 100644 index 0000000..63be7ed --- /dev/null +++ b/src/Controllers/TokenController.php @@ -0,0 +1,40 @@ +settings = $settings; + } + + public function handle(ServerRequestInterface $request): ResponseInterface + { + $oauth = new OAuth($this->settings); + $server = $oauth->server(); + + $body = $server->handleTokenRequest($oauth->request()::createFromGlobals()) + ->getResponseBody(); + return new JsonResponse(json_decode($body, true)); + } +} diff --git a/src/Middlewares/ResourceScopeMiddleware.php b/src/Middlewares/ResourceScopeMiddleware.php index 288acbd..951cbd4 100644 --- a/src/Middlewares/ResourceScopeMiddleware.php +++ b/src/Middlewares/ResourceScopeMiddleware.php @@ -4,50 +4,63 @@ namespace FoskyM\OAuthCenter\Middlewares; use Flarum\Foundation\ErrorHandling\ExceptionHandler\IlluminateValidationExceptionHandler; use Flarum\Foundation\ErrorHandling\JsonApiFormatter; +use Flarum\Settings\SettingsRepositoryInterface; +use Flarum\User\User; use FoskyM\OAuthCenter\OAuth; use FoskyM\OAuthCenter\Storage; use Illuminate\Support\Arr; +use Illuminate\Support\Str; use Illuminate\Validation\ValidationException; +use Laminas\Diactoros\Response\JsonResponse; use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; use Flarum\Http\RequestUtil; -use Flarum\Api\JsonApiResponse; -use Tobscure\JsonApi\Document; -use Tobscure\JsonApi\Exception\Handler\ResponseBag; - use FoskyM\OAuthCenter\Models\Scope; + class ResourceScopeMiddleware implements MiddlewareInterface { + const TOKEN_PREFIX = 'Bearer '; + protected $settings; + public function __construct(SettingsRepositoryInterface $settings) + { + $this->settings = $settings; + } public function process(Request $request, RequestHandlerInterface $handler): Response { - $path = $request->getUri()->getPath(); - $token = Arr::get($request->getQueryParams(), 'access_token', ''); + if (!$request->getAttribute('originalUri')) { + return $handler->handle($request); + } + + $headerLine = $request->getHeaderLine('authorization'); + + $parts = explode(';', $headerLine); + + if (isset($parts[0]) && Str::startsWith($parts[0], self::TOKEN_PREFIX)) { + $token = substr($parts[0], strlen(self::TOKEN_PREFIX)); + } else { + $token = Arr::get($request->getQueryParams(), 'access_token', ''); + } + $path = $request->getAttribute('originalUri')->getPath(); + if ($token !== '' && $scope = Scope::get_path_scope($path)) { if (strtolower($request->getMethod()) === strtolower($scope->method)) { try { - $oauth = new OAuth(); + $oauth = new OAuth($this->settings); $server = $oauth->server(); - $request = $oauth->request(); - if (!$server->verifyResourceRequest($request::createFromGlobals(), null, $scope->scope)) { - $server->getResponse()->send('json'); - die; - } - /*$error = new ResponseBag('422', [ - [ - 'status' => '422', - 'code' => 'validation_error', - 'source' => [ - 'pointer' => $path, - ], - 'detail' => 'Yikes! The access token don\'t has the scope.', - ], - ]); - $document = new Document(); - $document->setErrors($error->getErrors()); + $oauth_request = $oauth->request()::createFromGlobals(); - return new JsonApiResponse($document, $error->getStatus());*/ + if (!$server->verifyResourceRequest($oauth_request, null, $scope->scope)) { + return new JsonResponse(json_decode($server->getResponse()->getResponseBody(), true)); + } + + $token = $server->getAccessTokenData($oauth_request); + $actor = User::find($token['user_id']); + + $request = RequestUtil::withActor($request, $actor); + $request = $request->withAttribute('bypassCsrfToken', true); + $request = $request->withoutAttribute('session'); } catch (ValidationException $exception) { $handler = resolve(IlluminateValidationExceptionHandler::class); diff --git a/src/Middlewares/UnsetCsrfMiddleware.php b/src/Middlewares/UnsetCsrfMiddleware.php new file mode 100644 index 0000000..f1a2d72 --- /dev/null +++ b/src/Middlewares/UnsetCsrfMiddleware.php @@ -0,0 +1,35 @@ +getUri()->getPath(); + if (in_array($path, $uri)) { + $request = $request->withAttribute('bypassCsrfToken', true); + } + + return $handler->handle($request); + } +} diff --git a/src/Models/AccessToken.php b/src/Models/AccessToken.php index 502d559..4204e88 100644 --- a/src/Models/AccessToken.php +++ b/src/Models/AccessToken.php @@ -15,4 +15,5 @@ use Flarum\Database\AbstractModel; class AccessToken extends AbstractModel { protected $table = 'oauth_access_tokens'; + protected $guarded = []; } diff --git a/src/Models/AuthorizationCode.php b/src/Models/AuthorizationCode.php index 8b34f73..f144175 100644 --- a/src/Models/AuthorizationCode.php +++ b/src/Models/AuthorizationCode.php @@ -15,4 +15,5 @@ use Flarum\Database\AbstractModel; class AuthorizationCode extends AbstractModel { protected $table = 'oauth_authorization_codes'; + protected $guarded = []; } diff --git a/src/Models/Client.php b/src/Models/Client.php index 840b713..d6dc3d2 100644 --- a/src/Models/Client.php +++ b/src/Models/Client.php @@ -15,4 +15,16 @@ use Flarum\Database\AbstractModel; class Client extends AbstractModel { protected $table = 'oauth_clients'; + protected $guarded = []; + + public static function build(string $client_id, string $client_secret, int $user_id) + { + $client = new static(); + + $client->client_id = $client_id; + $client->client_secret = $client_secret; + $client->user_id = $user_id; + + return $client; + } } diff --git a/src/Models/Jwt.php b/src/Models/Jwt.php index d9ee703..652a3c4 100644 --- a/src/Models/Jwt.php +++ b/src/Models/Jwt.php @@ -15,4 +15,5 @@ use Flarum\Database\AbstractModel; class Jwt extends AbstractModel { protected $table = 'oauth_jwt'; + protected $guarded = []; } diff --git a/src/Models/RefreshToken.php b/src/Models/RefreshToken.php index 7f6bea0..92919f4 100644 --- a/src/Models/RefreshToken.php +++ b/src/Models/RefreshToken.php @@ -15,4 +15,5 @@ use Flarum\Database\AbstractModel; class RefreshToken extends AbstractModel { protected $table = 'oauth_refresh_tokens'; + protected $guarded = []; } diff --git a/src/Models/Scope.php b/src/Models/Scope.php index 125bf94..b191afc 100644 --- a/src/Models/Scope.php +++ b/src/Models/Scope.php @@ -15,7 +15,7 @@ use Flarum\Database\AbstractModel; class Scope extends AbstractModel { protected $table = 'oauth_scopes'; - + protected $guarded = []; static public function get_path_scope($path = '') { return self::where('resource_path', 'like', $path . '%')->first(); diff --git a/src/OAuth.php b/src/OAuth.php index 8718df0..66c67a8 100644 --- a/src/OAuth.php +++ b/src/OAuth.php @@ -36,9 +36,13 @@ class OAuth { return new Request; } + + public function storage(): Storage + { + return new Storage; + } public function server(): Server { - $storage = new Storage; $server = new Server($storage, array( 'allow_implicit' => $this->settings->get('foskym-oauth-center.allow_implicit') == "1", diff --git a/src/Storage.php b/src/Storage.php index e99fea1..c5c26c9 100644 --- a/src/Storage.php +++ b/src/Storage.php @@ -204,10 +204,10 @@ class Storage implements */ public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null, $code_challenge = null, $code_challenge_method = null) { - if (func_num_args() > 6) { + /*if (func_num_args() > 6) { // we are calling with an id token return call_user_func_array(array($this, 'setAuthorizationCodeWithIdToken'), func_get_args()); - } + }*/ // convert expires to datestring $expires = date('Y-m-d H:i:s', $expires); @@ -433,7 +433,7 @@ class Storage implements if ($result = Models\Scope::where('is_default', true)->get()) { $defaultScope = array_map(function ($row) { return $row['scope']; - }, $result); + }, $result->toArray()); return implode(' ', $defaultScope); }