Merge pull request #3 from FoskyM/dev-oauth

Dev oauth merged & the first package will be released
This commit is contained in:
FoskyM 2023-10-02 06:08:44 +08:00 committed by GitHub
commit f9e30d350e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
40 changed files with 1259 additions and 80 deletions

View file

@ -12,7 +12,10 @@
namespace FoskyM\OAuthCenter; namespace FoskyM\OAuthCenter;
use Flarum\Extend; use Flarum\Extend;
use Flarum\Http\Middleware\AuthenticateWithHeader;
use Flarum\Http\Middleware\CheckCsrfToken;
use FoskyM\OAuthCenter\Middlewares\ResourceScopeMiddleware; use FoskyM\OAuthCenter\Middlewares\ResourceScopeMiddleware;
use FoskyM\OAuthCenter\Middlewares\UnsetCsrfMiddleware;
return [ return [
(new Extend\Frontend('forum')) (new Extend\Frontend('forum'))
@ -26,9 +29,30 @@ return [
new Extend\Locales(__DIR__.'/locale'), new Extend\Locales(__DIR__.'/locale'),
(new Extend\Routes('forum')) (new Extend\Routes('forum'))
->post('/oauth/authorize', 'oauth.authorize.post', Controllers\AuthorizeController::class), ->post('/oauth/authorize', 'oauth.authorize.post', Controllers\AuthorizeController::class)
(new Extend\Routes('api')) ->post('/oauth/token', 'oauth.token', Controllers\TokenController::class),
->get('/oauth/clients', 'oauth.clients.list', Api\Controller\ListClientController::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),
]; ];

BIN
js/dist/admin.js generated vendored

Binary file not shown.

BIN
js/dist/admin.js.map generated vendored

Binary file not shown.

BIN
js/dist/forum.js generated vendored

Binary file not shown.

BIN
js/dist/forum.js.map generated vendored

Binary file not shown.

View file

@ -1,22 +1,100 @@
import app from 'flarum/admin/app'; import app from 'flarum/admin/app';
import Page from 'flarum/common/components/Page'; import Page from 'flarum/common/components/Page';
import Button from 'flarum/common/components/Button';
export default class ClientsPage extends Page { export default class ClientsPage extends Page {
settingName = 'collapsible-posts.reasons';
translationPrefix = 'foskym-oauth-center.admin.clients.'; translationPrefix = 'foskym-oauth-center.admin.clients.';
clients = [];
oninit(vnode) { oninit(vnode) {
super.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(); m.redraw();
}); });
} }
view() { view() {
return ( return (
<div> <div class={"OAuthCenter-clientsPage"}>
<h2>Clients Page</h2> {
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')))),
]),
]),
])
}
</div> </div>
); );
} }
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,
});
}
} }

View file

@ -1,11 +1,118 @@
import app from 'flarum/admin/app';
import Page from 'flarum/common/components/Page'; 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 { export default class ScopesPage extends 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() { view() {
return ( return (
<div> <div class={"OAuthCenter-scopesPage"}>
<h2>Scopes Page</h2> {
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')))),
]),
]),
])
}
</div> </div>
); );
} }
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,
});
}
} }

View file

@ -1,7 +1,9 @@
import Extend from 'flarum/common/extenders'; import Extend from 'flarum/common/extenders';
import Client from "./models/Client"; import Client from "./models/Client";
import Scope from "./models/Scope";
export default [ export default [
new Extend.Store() new Extend.Store()
.add('oauth-clients', Client), .add('oauth-clients', Client)
.add('oauth-scopes', Scope),
]; ];

View file

@ -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');
}

View file

@ -4,8 +4,18 @@ import Page from 'flarum/common/components/Page';
import IndexPage from 'flarum/forum/components/IndexPage'; import IndexPage from 'flarum/forum/components/IndexPage';
import LogInModal from 'flarum/forum/components/LogInModal'; import LogInModal from 'flarum/forum/components/LogInModal';
import extractText from 'flarum/common/utils/extractText'; 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 { export default class AuthorizePage extends IndexPage {
params = [];
client = null;
scopes = null;
client_scope = [];
loading = true;
is_authorized = false;
submit_loading = false;
oninit(vnode) { oninit(vnode) {
super.oninit(vnode); super.oninit(vnode);
if (!app.session.user) { if (!app.session.user) {
@ -13,18 +23,205 @@ export default class AuthorizePage extends IndexPage {
} }
const params = m.route.param(); 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() { setTitle() {
app.setTitle(extractText(app.translator.trans('foskym-oauth-center.forum.page.title.authorize'))); app.setTitle(extractText(app.translator.trans('foskym-oauth-center.forum.page.title.authorize')));
app.setTitleCount(0); app.setTitleCount(0);
} }
view() { view() {
if (!this.client) {
return '';
}
return ( return (
<div className="AuthorizePage"> !this.loading && <div className="AuthorizePage">
<div className="container"> <div className="container">
<div class="oauth-area">
<div class="oauth-main">
<div class="oauth-box oauth-header">
<h2>{app.forum.attribute('title')}</h2>
<p>
{app.translator.trans('foskym-oauth-center.forum.authorize.access')} <a
href={this.client.client_home()} target="_blank">{this.client.client_name()}</a>
</p>
</div>
<div class="oauth-box oauth-body">
<div class="oauth-top">
<img src={app.forum.attribute('faviconUrl')}/>
<i class="fas fa-exchange-alt fa-2x"></i>
<Tooltip text={this.client.client_desc()}>
<img src={this.client.client_icon()}/>
</Tooltip>
</div>
<div class="oauth-scope-area">
{
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 (
<div class="oauth-scope">
<div class="oauth-scope-left">
{
(scope_info.scope_icon().indexOf('fa-') > -1) ?
<i class={"oauth-scope-object fa-2x " + scope_info.scope_icon()}
style="margin-left:2px;color:#000"></i> :
<img class="oauth-scope-object" src={scope_info.scope_icon()} style="width:32px"/>
}
</div>
<div class="oauth-scope-body">
<h6 class="oauth-scope-heading">
{scope_info.scope_name()}
</h6>
<small>
{
scope_info.scope_desc()
.replace('{client_name}', this.client.client_name())
.replace('{user}', app.session.user.attribute('displayName'))
}
</small>
</div>
</div>
);
})
}
</div>
<form class="oauth-form" method="post" id="form" onsubmit={this.onsubmit.bind(this)}>
{/* <input type="hidden" name="response_type" value={this.params.response_type}/>
<input type="hidden" name="client_id" value={this.params.client_id}/>
<input type="hidden" name="redirect_uri"
value={this.params.redirect_uri}/>
<input type="hidden" name="state" value={this.params.state}/>
<input type="hidden" name="scope" value={this.params.scope}/>*/}
<input type="hidden" name="is_authorized" value={this.is_authorized}/>
<div style="display: flex; margin-top: 15px" class="oauth-form-item">
<Button className="Button" type="submit" style="width: 50%;" onclick={this.deny.bind(this)}
loading={this.submit_loading}>
{app.translator.trans('foskym-oauth-center.forum.authorize.deny')}
</Button>
<Button className="Button Button--primary" type="submit" style="width: 50%;"
onclick={this.agree.bind(this)} loading={this.submit_loading}>
{app.translator.trans('foskym-oauth-center.forum.authorize.agree')}
</Button>
</div>
</form>
</div>
</div>
</div>
</div> </div>
</div> </div>
); );
} }
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
}
} }

View file

@ -16,20 +16,19 @@ app.initializers.add('foskym/flarum-oauth-center', () => {
}; };
extend(UserPage.prototype, 'navItems', function (items) { extend(UserPage.prototype, 'navItems', function (items) {
if (app.session.user && app.session.user.id() === this.user.id()) { if (app.session.user && app.session.user.id() === this.user.id()) {
items.add( // items.add(
'authorized', // 'authorized',
LinkButton.component( // LinkButton.component(
{ // {
href: app.route('user.authorized', { username: this.user.username() }), // href: app.route('user.authorized', { username: this.user.username() }),
icon: 'fas fa-user-friends', // icon: 'fas fa-user-friends',
}, // },
[ // [
app.translator.trans('foskym-oauth-center.forum.page.label.authorized'), // app.translator.trans('foskym-oauth-center.forum.page.label.authorized'),
// this.user.moderatorNoteCount() > 0 ? <span className="Button-badge">{this.user.moderatorNoteCount()}</span> : '', // ]
] // ),
), // -110
-110 // );
);
} }
}); });
}); });

View file

@ -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) { @media (min-width: 992px) {
.OAuthCenter { .OAuthCenter {

View file

@ -0,0 +1 @@
@import url('./forum/oauth');

226
less/forum/oauth.less Normal file
View file

@ -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;
}

View file

@ -12,13 +12,23 @@ foskym-oauth-center:
clients: clients:
client_id: 应用 ID client_id: 应用 ID
client_secret: 应用密钥 client_secret: 应用密钥
redirect_uri: 回调地址(多地址请用空格分割) redirect_uri: 回调地址
grant_types: 授权类型(可空) grant_types: 授权类型
scope: 权限(可空) scope: 权限
name: 应用名称(可空) client_name: 名称
description: 应用描述(可空) client_desc: 描述
icon: 应用图标地址(可空 可使用fontawesome图标 client_icon: 图标
home: 主页地址(可空) client_home: 主页
add_button: 添加应用
scopes:
scope: 权限标识
resource_path: 资源路径
method: 请求方法
is_default: 默认
scope_name: 名称
scope_icon: 图标
scope_desc: 描述
add_button: 添加权限
forum: forum:
page: page:
@ -26,3 +36,7 @@ foskym-oauth-center:
authorize: 授权 authorize: 授权
label: label:
authorized: 授权记录 authorized: 授权记录
authorize:
access: 授权访问
agree: 授权
deny: 拒绝

View file

@ -0,0 +1,32 @@
<?php
/*
* This file is part of foskym/flarum-oauth-center.
*
* Copyright (c) 2023 FoskyM.
*
* For the full copyright and license information, please view the LICENSE.md
* file that was distributed with this source code.
*/
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Schema\Builder;
return [
'up' => 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) {
},
];

View file

@ -0,0 +1,29 @@
<?php
namespace FoskyM\OAuthCenter\Api\Controller;
use Flarum\Api\Controller\AbstractCreateController;
use Flarum\Http\RequestUtil;
use Illuminate\Support\Arr;
use Psr\Http\Message\ServerRequestInterface;
use Tobscure\JsonApi\Document;
use FoskyM\OAuthCenter\Models\Client;
use FoskyM\OAuthCenter\Api\Serializer\ClientSerializer;
class CreateClientController extends AbstractCreateController
{
public $serializer = ClientSerializer::class;
protected function data(ServerRequestInterface $request, Document $document)
{
$actor = RequestUtil::getActor($request);
$actor->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,
]);
}
}

View file

@ -0,0 +1,27 @@
<?php
namespace FoskyM\OAuthCenter\Api\Controller;
use Flarum\Api\Controller\AbstractCreateController;
use Flarum\Http\RequestUtil;
use Illuminate\Support\Arr;
use Psr\Http\Message\ServerRequestInterface;
use Tobscure\JsonApi\Document;
use FoskyM\OAuthCenter\Models\Scope;
use FoskyM\OAuthCenter\Api\Serializer\ScopeSerializer;
class CreateScopeController extends AbstractCreateController
{
public $serializer = ScopeSerializer::class;
protected function data(ServerRequestInterface $request, Document $document)
{
$actor = RequestUtil::getActor($request);
$actor->assertAdmin();
$attributes = Arr::get($request->getParsedBody(), 'data.attributes');
return Scope::create([
'scope' => Arr::get($attributes, 'scope'),
]);
}
}

View file

@ -0,0 +1,26 @@
<?php
namespace FoskyM\OAuthCenter\Api\Controller;
use Flarum\Api\Controller\AbstractDeleteController;
use Flarum\Http\RequestUtil;
use Illuminate\Support\Arr;
use Psr\Http\Message\ServerRequestInterface;
use Tobscure\JsonApi\Document;
use FoskyM\OAuthCenter\Models\Client;
use FoskyM\OAuthCenter\Api\Serializer\ClientSerializer;
class DeleteClientController extends AbstractDeleteController
{
public $serializer = ClientSerializer::class;
protected function delete(ServerRequestInterface $request)
{
$id = Arr::get($request->getQueryParams(), 'id');
RequestUtil::getActor($request)
->assertAdmin();
$client = Client::find($id);
$client->delete();
}
}

View file

@ -0,0 +1,25 @@
<?php
namespace FoskyM\OAuthCenter\Api\Controller;
use Flarum\Api\Controller\AbstractDeleteController;
use Flarum\Http\RequestUtil;
use Illuminate\Support\Arr;
use Psr\Http\Message\ServerRequestInterface;
use Tobscure\JsonApi\Document;
use FoskyM\OAuthCenter\Models\Scope;
use FoskyM\OAuthCenter\Api\Serializer\ScopeSerializer;
class DeleteScopeController extends AbstractDeleteController
{
protected function delete(ServerRequestInterface $request)
{
$id = Arr::get($request->getQueryParams(), 'id');
RequestUtil::getActor($request)
->assertAdmin();
$scope = Scope::find($id);
$scope->delete();
}
}

View file

@ -16,10 +16,8 @@ class ListClientController extends AbstractListController
protected function data(ServerRequestInterface $request, Document $document) protected function data(ServerRequestInterface $request, Document $document)
{ {
$actor = RequestUtil::getActor($request); $actor = RequestUtil::getActor($request);
if (!$actor->isAdmin()) { $actor->assertAdmin();
return [];
}
return Client::get(); return Client::all();
} }
} }

View file

@ -0,0 +1,23 @@
<?php
namespace FoskyM\OAuthCenter\Api\Controller;
use Flarum\Api\Controller\AbstractListController;
use Flarum\Http\RequestUtil;
use Illuminate\Support\Arr;
use Psr\Http\Message\ServerRequestInterface;
use Tobscure\JsonApi\Document;
use FoskyM\OAuthCenter\Models\Scope;
use FoskyM\OAuthCenter\Api\Serializer\ScopeSerializer;
class ListScopeController extends AbstractListController
{
public $serializer = ScopeSerializer::class;
protected function data(ServerRequestInterface $request, Document $document)
{
$actor = RequestUtil::getActor($request);
$actor->assertAdmin();
return Scope::all();
}
}

View file

@ -0,0 +1,26 @@
<?php
namespace FoskyM\OAuthCenter\Api\Controller;
use Flarum\Api\Controller\AbstractListController;
use Flarum\Http\RequestUtil;
use Illuminate\Support\Arr;
use Psr\Http\Message\ServerRequestInterface;
use Tobscure\JsonApi\Document;
use FoskyM\OAuthCenter\Models\Client;
use FoskyM\OAuthCenter\Api\Serializer\ClientPublicSerializer;
class ShowClientController extends AbstractListController
{
public $serializer = ClientPublicSerializer::class;
protected function data(ServerRequestInterface $request, Document $document)
{
$client_id = Arr::get($request->getQueryParams(), 'client_id');
RequestUtil::getActor($request)->assertRegistered();
$client = Client::where('client_id', $client_id)->get();
return $client;
}
}

View file

@ -0,0 +1,37 @@
<?php
namespace FoskyM\OAuthCenter\Api\Controller;
use Flarum\Api\Controller\AbstractListController;
use Flarum\Http\RequestUtil;
use Illuminate\Support\Arr;
use Psr\Http\Message\ServerRequestInterface;
use Tobscure\JsonApi\Document;
use FoskyM\OAuthCenter\Models\Client;
use FoskyM\OAuthCenter\Api\Serializer\ClientSerializer;
class UpdateClientController extends AbstractListController
{
public $serializer = ClientSerializer::class;
protected function data(ServerRequestInterface $request, Document $document)
{
$actor = RequestUtil::getActor($request);
$actor->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;
}
}

View file

@ -0,0 +1,37 @@
<?php
namespace FoskyM\OAuthCenter\Api\Controller;
use Flarum\Api\Controller\AbstractShowController;
use Flarum\Http\RequestUtil;
use Illuminate\Support\Arr;
use Psr\Http\Message\ServerRequestInterface;
use Tobscure\JsonApi\Document;
use FoskyM\OAuthCenter\Models\Scope;
use FoskyM\OAuthCenter\Api\Serializer\ScopeSerializer;
class UpdateScopeController extends AbstractShowController
{
public $serializer = ScopeSerializer::class;
protected function data(ServerRequestInterface $request, Document $document)
{
$actor = RequestUtil::getActor($request);
$actor->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;
}
}

View file

@ -0,0 +1,35 @@
<?php
namespace FoskyM\OAuthCenter\Api\Serializer;
use Flarum\Api\Serializer\AbstractSerializer;
use FoskyM\OAuthCenter\Models\Client;
use InvalidArgumentException;
class ClientPublicSerializer extends AbstractSerializer
{
protected $type = 'oauth-clients';
protected function getDefaultAttributes($model)
{
if (!($model instanceof Client)) {
throw new InvalidArgumentException(
get_class($this) . ' can only serialize instances of ' . Client::class
);
}
// See https://docs.flarum.org/extend/api.html#serializers for more information.
return [
"id" => $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
];
}
}

View file

@ -0,0 +1,34 @@
<?php
namespace FoskyM\OAuthCenter\Api\Serializer;
use Flarum\Api\Serializer\AbstractSerializer;
use FoskyM\OAuthCenter\Models\Scope;
use InvalidArgumentException;
class ScopeSerializer extends AbstractSerializer
{
protected $type = 'oauth-scopes';
protected function getDefaultAttributes($model)
{
if (!($model instanceof Scope)) {
throw new InvalidArgumentException(
get_class($this) . ' can only serialize instances of ' . Scope::class
);
}
// See https://docs.flarum.org/extend/api.html#serializers for more information.
return [
"id" => $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,
];
}
}

View file

@ -0,0 +1,49 @@
<?php
/*
* This file is part of foskym/flarum-oauth-center.
*
* Copyright (c) 2023 FoskyM.
*
* For the full copyright and license information, please view the LICENSE.md
* file that was distributed with this source code.
*/
namespace FoskyM\OAuthCenter\Controllers;
use Flarum\User\User;
use Flarum\Http\RequestUtil;
use FoskyM\OAuthCenter\OAuth;
use Illuminate\Support\Arr;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Laminas\Diactoros\Response\JsonResponse;
use Flarum\Settings\SettingsRepositoryInterface;
use Flarum\Group\Group;
class ApiUserController implements RequestHandlerInterface
{
protected $settings;
public function __construct(SettingsRepositoryInterface $settings)
{
$this->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);
}
}

View file

@ -35,23 +35,24 @@ class AuthorizeController implements RequestHandlerInterface
$params = $request->getParsedBody(); $params = $request->getParsedBody();
$oauth = new OAuth(); $oauth = new OAuth($this->settings);
$server = $oauth->server(); $server = $oauth->server();
$request = $oauth->request()::createFromGlobals(); $request = $oauth->request()::createFromGlobals();
$response = $oauth->response(); $response = $oauth->response();
if (!$server->validateAuthorizeRequest($request, $response)) { if (!$server->validateAuthorizeRequest($request, $response)) {
$response->send(); return new JsonResponse(json_decode($response->getResponseBody(), true));
die;
} }
$is_authorized = (Arr::get($params, 'authorized', 'no') === 'yes'); $is_authorized = Arr::get($params, 'is_authorized', 0);
$server->handleAuthorizeRequest($request, $response, $is_authorized, $actor->id); $server->handleAuthorizeRequest($request, $response, $is_authorized, $actor->id);
if ($is_authorized) { 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);
/* $code = substr($response->getHttpHeader('Location'), strpos($response->getHttpHeader('Location'), 'code=')+5, 40); return new JsonResponse([
exit("SUCCESS! Authorization Code: $code");*/ 'code' => $code
]);
} }
$response->send();
return new JsonResponse(json_decode($response->getResponseBody(), true));
} }
} }

View file

@ -0,0 +1,40 @@
<?php
/*
* This file is part of foskym/flarum-oauth-center.
*
* Copyright (c) 2023 FoskyM.
*
* For the full copyright and license information, please view the LICENSE.md
* file that was distributed with this source code.
*/
namespace FoskyM\OAuthCenter\Controllers;
use Flarum\User\User;
use Flarum\Http\RequestUtil;
use FoskyM\OAuthCenter\OAuth;
use Illuminate\Support\Arr;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Laminas\Diactoros\Response\JsonResponse;
use Flarum\Settings\SettingsRepositoryInterface;
use Flarum\Group\Group;
class TokenController implements RequestHandlerInterface
{
protected $settings;
public function __construct(SettingsRepositoryInterface $settings)
{
$this->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));
}
}

View file

@ -4,50 +4,63 @@ namespace FoskyM\OAuthCenter\Middlewares;
use Flarum\Foundation\ErrorHandling\ExceptionHandler\IlluminateValidationExceptionHandler; use Flarum\Foundation\ErrorHandling\ExceptionHandler\IlluminateValidationExceptionHandler;
use Flarum\Foundation\ErrorHandling\JsonApiFormatter; use Flarum\Foundation\ErrorHandling\JsonApiFormatter;
use Flarum\Settings\SettingsRepositoryInterface;
use Flarum\User\User;
use FoskyM\OAuthCenter\OAuth; use FoskyM\OAuthCenter\OAuth;
use FoskyM\OAuthCenter\Storage; use FoskyM\OAuthCenter\Storage;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException; use Illuminate\Validation\ValidationException;
use Laminas\Diactoros\Response\JsonResponse;
use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface; use Psr\Http\Server\RequestHandlerInterface;
use Flarum\Http\RequestUtil; use Flarum\Http\RequestUtil;
use Flarum\Api\JsonApiResponse;
use Tobscure\JsonApi\Document;
use Tobscure\JsonApi\Exception\Handler\ResponseBag;
use FoskyM\OAuthCenter\Models\Scope; use FoskyM\OAuthCenter\Models\Scope;
class ResourceScopeMiddleware implements MiddlewareInterface 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 public function process(Request $request, RequestHandlerInterface $handler): Response
{ {
$path = $request->getUri()->getPath(); 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', ''); $token = Arr::get($request->getQueryParams(), 'access_token', '');
}
$path = $request->getAttribute('originalUri')->getPath();
if ($token !== '' && $scope = Scope::get_path_scope($path)) { if ($token !== '' && $scope = Scope::get_path_scope($path)) {
if (strtolower($request->getMethod()) === strtolower($scope->method)) { if (strtolower($request->getMethod()) === strtolower($scope->method)) {
try { try {
$oauth = new OAuth(); $oauth = new OAuth($this->settings);
$server = $oauth->server(); $server = $oauth->server();
$request = $oauth->request(); $oauth_request = $oauth->request()::createFromGlobals();
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());
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) { } catch (ValidationException $exception) {
$handler = resolve(IlluminateValidationExceptionHandler::class); $handler = resolve(IlluminateValidationExceptionHandler::class);

View file

@ -0,0 +1,35 @@
<?php
namespace FoskyM\OAuthCenter\Middlewares;
use Flarum\Foundation\ErrorHandling\ExceptionHandler\IlluminateValidationExceptionHandler;
use Flarum\Foundation\ErrorHandling\JsonApiFormatter;
use FoskyM\OAuthCenter\OAuth;
use FoskyM\OAuthCenter\Storage;
use Illuminate\Support\Arr;
use Illuminate\Validation\ValidationException;
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 UnsetCsrfMiddleware implements MiddlewareInterface
{
public function process(Request $request, RequestHandlerInterface $handler): Response
{
$uri = [
'/oauth/token',
];
$path = $request->getUri()->getPath();
if (in_array($path, $uri)) {
$request = $request->withAttribute('bypassCsrfToken', true);
}
return $handler->handle($request);
}
}

View file

@ -15,4 +15,5 @@ use Flarum\Database\AbstractModel;
class AccessToken extends AbstractModel class AccessToken extends AbstractModel
{ {
protected $table = 'oauth_access_tokens'; protected $table = 'oauth_access_tokens';
protected $guarded = [];
} }

View file

@ -15,4 +15,5 @@ use Flarum\Database\AbstractModel;
class AuthorizationCode extends AbstractModel class AuthorizationCode extends AbstractModel
{ {
protected $table = 'oauth_authorization_codes'; protected $table = 'oauth_authorization_codes';
protected $guarded = [];
} }

View file

@ -15,4 +15,16 @@ use Flarum\Database\AbstractModel;
class Client extends AbstractModel class Client extends AbstractModel
{ {
protected $table = 'oauth_clients'; 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;
}
} }

View file

@ -15,4 +15,5 @@ use Flarum\Database\AbstractModel;
class Jwt extends AbstractModel class Jwt extends AbstractModel
{ {
protected $table = 'oauth_jwt'; protected $table = 'oauth_jwt';
protected $guarded = [];
} }

View file

@ -15,4 +15,5 @@ use Flarum\Database\AbstractModel;
class RefreshToken extends AbstractModel class RefreshToken extends AbstractModel
{ {
protected $table = 'oauth_refresh_tokens'; protected $table = 'oauth_refresh_tokens';
protected $guarded = [];
} }

View file

@ -15,7 +15,7 @@ use Flarum\Database\AbstractModel;
class Scope extends AbstractModel class Scope extends AbstractModel
{ {
protected $table = 'oauth_scopes'; protected $table = 'oauth_scopes';
protected $guarded = [];
static public function get_path_scope($path = '') static public function get_path_scope($path = '')
{ {
return self::where('resource_path', 'like', $path . '%')->first(); return self::where('resource_path', 'like', $path . '%')->first();

View file

@ -36,9 +36,13 @@ class OAuth
{ {
return new Request; return new Request;
} }
public function storage(): Storage
{
return new Storage;
}
public function server(): Server public function server(): Server
{ {
$storage = new Storage; $storage = new Storage;
$server = new Server($storage, array( $server = new Server($storage, array(
'allow_implicit' => $this->settings->get('foskym-oauth-center.allow_implicit') == "1", 'allow_implicit' => $this->settings->get('foskym-oauth-center.allow_implicit') == "1",

View file

@ -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) 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 // we are calling with an id token
return call_user_func_array(array($this, 'setAuthorizationCodeWithIdToken'), func_get_args()); return call_user_func_array(array($this, 'setAuthorizationCodeWithIdToken'), func_get_args());
} }*/
// convert expires to datestring // convert expires to datestring
$expires = date('Y-m-d H:i:s', $expires); $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()) { if ($result = Models\Scope::where('is_default', true)->get()) {
$defaultScope = array_map(function ($row) { $defaultScope = array_map(function ($row) {
return $row['scope']; return $row['scope'];
}, $result); }, $result->toArray());
return implode(' ', $defaultScope); return implode(' ', $defaultScope);
} }