OAuth2 provider implemented
This commit is contained in:
parent
c7d348b27d
commit
1f54626a33
@ -50,7 +50,7 @@ exports.up = function(knex, Promise) {
|
||||
table.text('redirect_url')
|
||||
table.text('icon')
|
||||
table.text('secret')
|
||||
table.text('scopes')
|
||||
table.text('scope')
|
||||
table.text('grants')
|
||||
|
||||
table.integer('user_id').unsigned().notNullable()
|
||||
@ -66,6 +66,7 @@ exports.up = function(knex, Promise) {
|
||||
|
||||
table.integer('user_id').unsigned().notNullable()
|
||||
table.integer('client_id').unsigned().notNullable()
|
||||
table.text('scope')
|
||||
table.dateTime('expires_at')
|
||||
|
||||
table.foreign('user_id').references('users.id').onDelete('CASCADE').onUpdate('CASCADE')
|
||||
@ -77,7 +78,7 @@ exports.up = function(knex, Promise) {
|
||||
table.integer('user_id').unsigned().notNullable()
|
||||
table.integer('client_id').unsigned().notNullable()
|
||||
table.text('code')
|
||||
table.text('scopes')
|
||||
table.text('scope')
|
||||
|
||||
table.foreign('user_id').references('users.id').onDelete('CASCADE').onUpdate('CASCADE')
|
||||
|
||||
@ -90,7 +91,7 @@ exports.up = function(knex, Promise) {
|
||||
table.integer('user_id').unsigned().notNullable()
|
||||
table.integer('client_id').unsigned().notNullable()
|
||||
table.text('token')
|
||||
table.text('scopes')
|
||||
table.text('scope')
|
||||
|
||||
table.foreign('user_id').references('users.id').onDelete('CASCADE').onUpdate('CASCADE')
|
||||
|
||||
@ -103,7 +104,7 @@ exports.up = function(knex, Promise) {
|
||||
table.integer('user_id').unsigned().notNullable()
|
||||
table.integer('client_id').unsigned().notNullable()
|
||||
table.text('token')
|
||||
table.text('scopes')
|
||||
table.text('scope')
|
||||
|
||||
table.foreign('user_id').references('users.id').onDelete('CASCADE').onUpdate('CASCADE')
|
||||
|
||||
|
@ -32,7 +32,7 @@ class OAuth2AuthorizedClient extends Model {
|
||||
|
||||
class OAuth2Code extends Model {
|
||||
static get tableName () {
|
||||
return 'oauth2_client_authorization'
|
||||
return 'oauth2_code'
|
||||
}
|
||||
}
|
||||
|
||||
|
152
server/api/oauth2/controller/authorization.js
Normal file
152
server/api/oauth2/controller/authorization.js
Normal file
@ -0,0 +1,152 @@
|
||||
import error from '../error'
|
||||
import response from '../response'
|
||||
import model from '../model'
|
||||
import authorization from './code'
|
||||
import wrap from '../../../../scripts/asyncRoute'
|
||||
|
||||
const usermodel = model.user
|
||||
|
||||
module.exports = wrap(async (req, res, next) => {
|
||||
let clientId = null
|
||||
let redirectUri = null
|
||||
let responseType = null
|
||||
let grantType = null
|
||||
let scope = null
|
||||
let user = null
|
||||
|
||||
if (!req.query.redirect_uri) {
|
||||
return response.error(req, res, new error.InvalidRequest('redirect_uri field is mandatory for authorization endpoint'), redirectUri)
|
||||
}
|
||||
|
||||
redirectUri = req.query.redirect_uri
|
||||
console.debug('Parameter redirect uri is', redirectUri)
|
||||
|
||||
if (!req.query.client_id) {
|
||||
return response.error(req, res, new error.InvalidRequest('client_id field is mandatory for authorization endpoint'), redirectUri)
|
||||
}
|
||||
|
||||
// Check for client_secret (prevent passing it)
|
||||
if (req.query.client_secret) {
|
||||
return response.error(req, res, new error.InvalidRequest('client_secret field should not be passed to the authorization endpoint'), redirectUri)
|
||||
}
|
||||
|
||||
clientId = req.query.client_id
|
||||
console.debug('Parameter client_id is', clientId)
|
||||
|
||||
if (!req.query.response_type) {
|
||||
return response.error(req, res, new error.InvalidRequest('response_type field is mandatory for authorization endpoint'), redirectUri)
|
||||
}
|
||||
|
||||
responseType = req.query.response_type
|
||||
console.debug('Parameter response_type is', responseType)
|
||||
|
||||
switch (responseType) {
|
||||
case 'code':
|
||||
grantType = 'authorization_code'
|
||||
break
|
||||
case 'token':
|
||||
grantType = 'implicit'
|
||||
break
|
||||
default:
|
||||
return response.error(req, res, new error.UnsupportedResponseType('Unknown response_type parameter passed'), redirectUri)
|
||||
}
|
||||
console.debug('Parameter response_type is', responseType)
|
||||
|
||||
let client = await req.oauth2.model.client.fetchById(clientId)
|
||||
if (!client) {
|
||||
return response.error(req, res, new error.InvalidClient('Client not found'), redirectUri)
|
||||
}
|
||||
|
||||
if (!req.oauth2.model.client.getRedirectUri(client)) {
|
||||
return response.error(req, res, new error.UnsupportedResponseType('The client has not set a redirect uri'), redirectUri)
|
||||
} else if (!req.oauth2.model.client.checkRedirectUri(client, redirectUri)) {
|
||||
return response.error(req, res, new error.InvalidRequest('Wrong RedirectUri provided'), redirectUri)
|
||||
} else {
|
||||
console.debug('redirect_uri check passed')
|
||||
}
|
||||
|
||||
if (!req.oauth2.model.client.checkGrantType(client, grantType)) {
|
||||
return response.error(req, res, new error.UnauthorizedClient('This client does not support this grant type'), redirectUri)
|
||||
} else {
|
||||
console.debug('Grant type check passed')
|
||||
}
|
||||
|
||||
scope = req.oauth2.model.client.transformScope(req.query.scope)
|
||||
scope = req.oauth2.model.client.checkScope(client, scope)
|
||||
if (!scope) {
|
||||
return response.error(req, res, new error.InvalidScope('Client does not allow access to this scope'), redirectUri)
|
||||
} else {
|
||||
console.debug('Scope check passed')
|
||||
}
|
||||
|
||||
user = await req.oauth2.model.user.fetchFromRequest(req)
|
||||
if (!user) {
|
||||
return response.error(req, res, new error.InvalidRequest('There is no currently logged in user'), redirectUri)
|
||||
} else {
|
||||
if (!user.username) {
|
||||
return response.error(req, res, new error.Forbidden(user), redirectUri)
|
||||
}
|
||||
console.debug('User fetched from request')
|
||||
}
|
||||
|
||||
let data = null
|
||||
|
||||
if (req.method === 'GET') {
|
||||
let hasAuthorizedAlready = await usermodel.clientAllowed(user.id, client.id, scope)
|
||||
if (client.verified === 1) {
|
||||
hasAuthorizedAlready = true
|
||||
}
|
||||
|
||||
if (hasAuthorizedAlready) {
|
||||
if (grantType === 'authorization_code') {
|
||||
try {
|
||||
data = await authorization.Code(req, res, client, scope, user, redirectUri, false)
|
||||
} catch (err) {
|
||||
return response.error(req, res, err, redirectUri)
|
||||
}
|
||||
|
||||
return response.data(req, res, { code: data }, redirectUri)
|
||||
} else if (grantType === 'implicit') {
|
||||
try {
|
||||
data = await authorization.Implicit(req, res, client, scope, user, redirectUri, false)
|
||||
} catch (err) {
|
||||
return response.error(req, res, err, redirectUri)
|
||||
}
|
||||
|
||||
return response.data(req, res, {
|
||||
token_type: 'bearer',
|
||||
access_token: data,
|
||||
expires_in: req.oauth2.model.accessToken.ttl
|
||||
}, redirectUri)
|
||||
}
|
||||
} else {
|
||||
return req.oauth2.decision(req, res, client, scope, user, redirectUri)
|
||||
}
|
||||
|
||||
return response.error(req, res, new error.InvalidRequest('Invalid request method'), redirectUri)
|
||||
}
|
||||
|
||||
if (grantType === 'authorization_code') {
|
||||
try {
|
||||
data = await authorization.Code(req, res, client, scope, user, redirectUri, true)
|
||||
} catch (err) {
|
||||
return response.error(req, res, err, redirectUri)
|
||||
}
|
||||
|
||||
return response.data(req, res, { code: data }, redirectUri)
|
||||
} else if (grantType === 'implicit') {
|
||||
try {
|
||||
data = await authorization.Implicit(req, res, client, scope, user, redirectUri, true)
|
||||
} catch (err) {
|
||||
return response.error(req, res, err, redirectUri)
|
||||
}
|
||||
|
||||
return response.data(req, res, {
|
||||
token_type: 'bearer',
|
||||
access_token: data,
|
||||
expires_in: req.oauth2.model.accessToken.ttl
|
||||
}, redirectUri)
|
||||
} else {
|
||||
return response.error(req, res, new error.InvalidRequest('Invalid request method'), redirectUri)
|
||||
}
|
||||
})
|
28
server/api/oauth2/controller/code/code.js
Normal file
28
server/api/oauth2/controller/code/code.js
Normal file
@ -0,0 +1,28 @@
|
||||
import error from '../../error'
|
||||
import model from '../../model'
|
||||
|
||||
module.exports = async (req, res, client, scope, user, redirectUri, createAllowFuture) => {
|
||||
let codeValue = null
|
||||
|
||||
if (createAllowFuture) {
|
||||
if (!req.body || (typeof req.body['decision']) === 'undefined') {
|
||||
throw new error.InvalidRequest('No decision parameter passed')
|
||||
} else if (req.body['decision'] === '0') {
|
||||
throw new error.AccessDenied('User denied access to the resource')
|
||||
} else {
|
||||
console.debug('Decision check passed')
|
||||
}
|
||||
|
||||
await model.user.allowClient(user.id, client.id, scope)
|
||||
}
|
||||
|
||||
try {
|
||||
codeValue = await req.oauth2.model.code.create(req.oauth2.model.user.getId(user),
|
||||
req.oauth2.model.client.getId(client), scope, req.oauth2.model.code.ttl)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
throw new error.ServerError('Failed to call code.create function')
|
||||
}
|
||||
|
||||
return codeValue
|
||||
}
|
27
server/api/oauth2/controller/code/implicit.js
Normal file
27
server/api/oauth2/controller/code/implicit.js
Normal file
@ -0,0 +1,27 @@
|
||||
import error from '../../error'
|
||||
import model from '../../model'
|
||||
|
||||
module.exports = async (req, res, client, scope, user, redirectUri, createAllowFuture) => {
|
||||
let accessTokenValue = null
|
||||
|
||||
if (createAllowFuture) {
|
||||
if (!req.body || (typeof req.body['decision']) === 'undefined') {
|
||||
throw new error.InvalidRequest('No decision parameter passed')
|
||||
} else if (req.body['decision'] === 0) {
|
||||
throw new error.AccessDenied('User denied access to the resource')
|
||||
} else {
|
||||
console.debug('Decision check passed')
|
||||
}
|
||||
|
||||
await model.user.allowClient(user.id, client.id, scope)
|
||||
}
|
||||
|
||||
try {
|
||||
accessTokenValue = await req.oauth2.model.accessToken.create(req.oauth2.model.user.getId(user),
|
||||
req.oauth2.model.client.getId(client), scope, req.oauth2.model.accessToken.ttl)
|
||||
} catch (err) {
|
||||
throw new error.ServerError('Failed to call accessToken.create function')
|
||||
}
|
||||
|
||||
return accessTokenValue
|
||||
}
|
4
server/api/oauth2/controller/code/index.js
Normal file
4
server/api/oauth2/controller/code/index.js
Normal file
@ -0,0 +1,4 @@
|
||||
module.exports = {
|
||||
Code: require('./code'),
|
||||
Implicit: require('./implicit')
|
||||
}
|
5
server/api/oauth2/controller/decision.js
Normal file
5
server/api/oauth2/controller/decision.js
Normal file
@ -0,0 +1,5 @@
|
||||
module.exports = function (req, res, client, scope, user) {
|
||||
res.locals.client = client
|
||||
res.locals.scope = scope
|
||||
res.render('authorization')
|
||||
}
|
5
server/api/oauth2/controller/index.js
Normal file
5
server/api/oauth2/controller/index.js
Normal file
@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
authorization: require('./authorization'),
|
||||
introspection: require('./introspection'),
|
||||
token: require('./token')
|
||||
}
|
54
server/api/oauth2/controller/introspection.js
Normal file
54
server/api/oauth2/controller/introspection.js
Normal file
@ -0,0 +1,54 @@
|
||||
import error from '../error'
|
||||
import response from '../response'
|
||||
import wrap from '../../../../scripts/asyncRoute'
|
||||
|
||||
module.exports = wrap(async function (req, res) {
|
||||
let clientId = null
|
||||
let clientSecret = null
|
||||
|
||||
if (req.body.client_id && req.body.client_secret) {
|
||||
clientId = req.body.client_id
|
||||
clientSecret = req.body.client_secret
|
||||
console.debug('Client credentials parsed from body parameters ', clientId, clientSecret)
|
||||
} else {
|
||||
if (!req.headers || !req.headers.authorization) {
|
||||
return response.error(req, res, new error.InvalidRequest('No authorization header passed'))
|
||||
}
|
||||
|
||||
let pieces = req.headers.authorization.split(' ', 2)
|
||||
if (!pieces || pieces.length !== 2) {
|
||||
return response.error(req, res, new error.InvalidRequest('Authorization header is corrupted'))
|
||||
}
|
||||
|
||||
if (pieces[0] !== 'Basic') {
|
||||
return response.error(req, res, new error.InvalidRequest('Unsupported authorization method:', pieces[0]))
|
||||
}
|
||||
|
||||
pieces = new Buffer(pieces[1], 'base64').toString('ascii').split(':', 2)
|
||||
if (!pieces || pieces.length !== 2) {
|
||||
return response.error(req, res, new error.InvalidRequest('Authorization header has corrupted data'))
|
||||
}
|
||||
|
||||
clientId = pieces[0]
|
||||
clientSecret = pieces[1]
|
||||
console.debug('Client credentials parsed from basic auth header: ', clientId, clientSecret)
|
||||
}
|
||||
|
||||
if (!req.body.token) {
|
||||
return response.error(req, res, new error.InvalidRequest('Token not provided in request body'))
|
||||
}
|
||||
|
||||
let token = await req.oauth2.model.accessToken.fetchByToken(req.body.token)
|
||||
if (!token) {
|
||||
return response.error(req, res, new error.InvalidRequest('Token does not exist'))
|
||||
}
|
||||
|
||||
let ttl = req.oauth2.model.accessToken.getTTL(token)
|
||||
let resObj = {
|
||||
token_type: 'bearer',
|
||||
token: token.token,
|
||||
expires_in: Math.floor(ttl / 1000)
|
||||
}
|
||||
|
||||
response.data(req, res, resObj)
|
||||
})
|
88
server/api/oauth2/controller/token.js
Normal file
88
server/api/oauth2/controller/token.js
Normal file
@ -0,0 +1,88 @@
|
||||
import token from './tokens'
|
||||
import error from '../error'
|
||||
import response from '../response'
|
||||
import wrap from '../../../../scripts/asyncRoute'
|
||||
|
||||
module.exports = wrap(async (req, res) => {
|
||||
let clientId = null
|
||||
let clientSecret = null
|
||||
let grantType = null
|
||||
|
||||
if (req.body.client_id && req.body.client_secret) {
|
||||
clientId = req.body.client_id
|
||||
clientSecret = req.body.client_secret
|
||||
console.debug('Client credentials parsed from body parameters', clientId, clientSecret)
|
||||
} else {
|
||||
if (!req.headers || !req.headers.authorization) {
|
||||
return response.error(req, res, new error.InvalidRequest('No authorization header passed'))
|
||||
}
|
||||
|
||||
let pieces = req.headers.authorization.split(' ', 2)
|
||||
if (!pieces || pieces.length !== 2) {
|
||||
return response.error(req, res, new error.InvalidRequest('Authorization header is corrupted'))
|
||||
}
|
||||
|
||||
if (pieces[0] !== 'Basic') {
|
||||
return response.error(req, res, new error.InvalidRequest('Unsupported authorization method:', pieces[0]))
|
||||
}
|
||||
|
||||
pieces = new Buffer(pieces[1], 'base64').toString('ascii').split(':', 2)
|
||||
if (!pieces || pieces.length !== 2) {
|
||||
return response.error(req, res, new error.InvalidRequest('Authorization header has corrupted data'))
|
||||
}
|
||||
|
||||
clientId = pieces[0]
|
||||
clientSecret = pieces[1]
|
||||
console.debug('Client credentials parsed from basic auth header:', clientId, clientSecret)
|
||||
}
|
||||
|
||||
if (!req.body.grant_type) {
|
||||
return response.error(req, res, new error.InvalidRequest('Request body does not contain grant_type parameter'))
|
||||
}
|
||||
|
||||
grantType = req.body.grant_type
|
||||
console.debug('Parameter grant_type is', grantType)
|
||||
|
||||
let client = await req.oauth2.model.client.fetchById(clientId)
|
||||
|
||||
if (!client) {
|
||||
return response.error(req, res, new error.InvalidClient('Client not found'))
|
||||
}
|
||||
|
||||
let valid = req.oauth2.model.client.checkSecret(client, clientSecret)
|
||||
if (!valid) {
|
||||
return response.error(req, res, new error.UnauthorizedClient('Invalid client secret'))
|
||||
}
|
||||
|
||||
if (!req.oauth2.model.client.checkGrantType(client, grantType) && grantType !== 'refresh_token') {
|
||||
return response.error(req, res, new error.UnauthorizedClient('Invalid grant type for the client'))
|
||||
} else {
|
||||
console.debug('Grant type check passed')
|
||||
}
|
||||
|
||||
let evt
|
||||
try {
|
||||
switch (grantType) {
|
||||
case 'authorization_code':
|
||||
evt = await token.authorizationCode(req.oauth2, client, req.body.code, req.body.redirect_uri)
|
||||
break
|
||||
case 'password':
|
||||
evt = await token.password(req.oauth2, client, req.body.username, req.body.password, req.body.scope)
|
||||
break
|
||||
case 'client_credentials':
|
||||
evt = await token.clientCredentials(req.oauth2, client, req.body.scope)
|
||||
break
|
||||
case 'refresh_token':
|
||||
evt = await token.refreshToken(req.oauth2, client, req.body.refresh_token, req.body.scope)
|
||||
break
|
||||
default:
|
||||
throw new error.UnsupportedGrantType('Grant type does not match any supported type')
|
||||
}
|
||||
|
||||
if (evt) {
|
||||
response.data(req, res, evt)
|
||||
}
|
||||
} catch (e) {
|
||||
response.error(req, res, e)
|
||||
}
|
||||
})
|
68
server/api/oauth2/controller/tokens/authorizationCode.js
Normal file
68
server/api/oauth2/controller/tokens/authorizationCode.js
Normal file
@ -0,0 +1,68 @@
|
||||
import error from '../../error'
|
||||
|
||||
module.exports = async (oauth2, client, providedCode, redirectUri) => {
|
||||
let respObj = {
|
||||
token_type: 'bearer'
|
||||
}
|
||||
|
||||
let code = null
|
||||
|
||||
try {
|
||||
code = await oauth2.model.code.fetchByCode(providedCode)
|
||||
} catch (err) {
|
||||
throw new error.ServerError('Failed to call code.fetchByCode function')
|
||||
}
|
||||
|
||||
if (code) {
|
||||
if (oauth2.model.code.getClientId(code) !== oauth2.model.client.getId(client)) {
|
||||
throw new error.InvalidGrant('Code was issued by another client')
|
||||
}
|
||||
|
||||
if (!oauth2.model.code.checkTTL(code)) {
|
||||
throw new error.InvalidGrant('Code has already expired')
|
||||
}
|
||||
} else {
|
||||
throw new error.InvalidGrant('Code not found')
|
||||
}
|
||||
|
||||
console.debug('Code fetched ', code)
|
||||
|
||||
try {
|
||||
await oauth2.model.refreshToken.removeByUserIdClientId(oauth2.model.code.getUserId(code), oauth2.model.code.getClientId(code))
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
throw new error.ServerError('Failed to call refreshToken.removeByUserIdClientId function')
|
||||
}
|
||||
|
||||
console.debug('Refresh token removed')
|
||||
|
||||
if (!oauth2.model.client.checkGrantType(client, 'refresh_token')) {
|
||||
console.debug('Client does not allow grant type refresh_token, skip creation')
|
||||
} else {
|
||||
try {
|
||||
respObj.refresh_token = await oauth2.model.refreshToken.create(oauth2.model.code.getUserId(code), oauth2.model.code.getClientId(code), oauth2.model.code.getScope(code))
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
throw new error.ServerError('Failed to call refreshToken.create function')
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
respObj.access_token = await oauth2.model.accessToken.create(oauth2.model.code.getUserId(code), oauth2.model.code.getClientId(code), oauth2.model.code.getScope(code), oauth2.model.accessToken.ttl)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
throw new error.ServerError('Failed to call accessToken.create function')
|
||||
}
|
||||
|
||||
respObj.expires_in = oauth2.model.accessToken.ttl
|
||||
console.debug('Access token saved: ', respObj.access_token)
|
||||
|
||||
try {
|
||||
await oauth2.model.code.removeByCode(providedCode)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
throw new error.ServerError('Failed to call code.removeByCode function')
|
||||
}
|
||||
|
||||
return respObj
|
||||
}
|
28
server/api/oauth2/controller/tokens/clientCredentials.js
Normal file
28
server/api/oauth2/controller/tokens/clientCredentials.js
Normal file
@ -0,0 +1,28 @@
|
||||
import error from '../../error'
|
||||
|
||||
module.exports = async (oauth2, client, wantScope) => {
|
||||
let scope = null
|
||||
|
||||
let resObj = {
|
||||
token_type: 'bearer'
|
||||
}
|
||||
|
||||
scope = oauth2.model.client.transformScope(wantScope)
|
||||
scope = oauth2.model.client.checkScope(client, scope)
|
||||
|
||||
if (!scope) {
|
||||
throw new error.InvalidScope('Client does not allow access to this scope')
|
||||
} else {
|
||||
console.debug('Scope check passed ', scope)
|
||||
}
|
||||
|
||||
try {
|
||||
resObj.access_token = await oauth2.model.accessToken.create(null, oauth2.model.client.getId(client), scope, oauth2.model.accessToken.ttl)
|
||||
} catch (err) {
|
||||
throw new error.ServerError('Failed to call accessToken.create function')
|
||||
}
|
||||
|
||||
resObj.expires_in = oauth2.model.accessToken.ttl
|
||||
|
||||
return resObj
|
||||
}
|
11
server/api/oauth2/controller/tokens/index.js
Normal file
11
server/api/oauth2/controller/tokens/index.js
Normal file
@ -0,0 +1,11 @@
|
||||
import authorizationCode from './authorizationCode'
|
||||
import clientCredentials from './clientCredentials'
|
||||
import password from './password'
|
||||
import refreshToken from './refreshToken'
|
||||
|
||||
module.exports = {
|
||||
authorizationCode: authorizationCode,
|
||||
clientCredentials: clientCredentials,
|
||||
password: password,
|
||||
refreshToken: refreshToken
|
||||
}
|
68
server/api/oauth2/controller/tokens/password.js
Normal file
68
server/api/oauth2/controller/tokens/password.js
Normal file
@ -0,0 +1,68 @@
|
||||
import error from '../../error'
|
||||
|
||||
module.exports = async (oauth2, client, username, password, scope) => {
|
||||
let user = null
|
||||
let resObj = {
|
||||
token_type: 'bearer'
|
||||
}
|
||||
|
||||
if (!username) {
|
||||
throw new error.InvalidRequest('Username is mandatory for password grant type')
|
||||
}
|
||||
|
||||
if (!password) {
|
||||
throw new error.InvalidRequest('Password is mandatory for password grant type')
|
||||
}
|
||||
|
||||
scope = oauth2.model.client.transformScope(scope)
|
||||
scope = oauth2.model.client.checkScope(client, scope)
|
||||
if (!scope) {
|
||||
throw new error.InvalidScope('Client does not allow access to this scope')
|
||||
} else {
|
||||
console.debug('Scope check passed: ', scope)
|
||||
}
|
||||
|
||||
try {
|
||||
user = await oauth2.model.user.fetchByUsername(username)
|
||||
} catch (err) {
|
||||
throw new error.ServerError('Failed to call user.fetchByUsername function')
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
throw new error.InvalidClient('User not found')
|
||||
}
|
||||
|
||||
let valid = await oauth2.model.user.checkPassword(user, password)
|
||||
if (!valid) {
|
||||
throw new error.InvalidClient('Wrong password')
|
||||
}
|
||||
|
||||
try {
|
||||
await oauth2.model.refreshToken.removeByUserIdClientId(oauth2.model.user.getId(user), oauth2.model.client.getId(client))
|
||||
} catch (err) {
|
||||
throw new error.ServerError('Failed to call refreshToken.removeByUserIdClientId function')
|
||||
}
|
||||
|
||||
console.debug('Refresh token removed')
|
||||
|
||||
if (!oauth2.model.client.checkGrantType(client, 'refresh_token')) {
|
||||
console.debug('Client does not allow grant type refresh_token, skip creation')
|
||||
} else {
|
||||
try {
|
||||
resObj.refresh_token = await oauth2.model.refreshToken.create(oauth2.model.user.getId(user), oauth2.model.client.getId(client), scope)
|
||||
} catch (err) {
|
||||
throw new error.ServerError('Failed to call refreshToken.create function')
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
resObj.access_token = await oauth2.model.accessToken.create(oauth2.model.user.getId(user), oauth2.model.client.getId(client), scope, oauth2.model.accessToken.ttl)
|
||||
} catch (err) {
|
||||
throw new error.ServerError('Failed to call accessToken.create function')
|
||||
}
|
||||
|
||||
resObj.expires_in = oauth2.model.accessToken.ttl
|
||||
console.debug('Access token saved ', resObj.access_token)
|
||||
|
||||
return resObj
|
||||
}
|
72
server/api/oauth2/controller/tokens/refreshToken.js
Normal file
72
server/api/oauth2/controller/tokens/refreshToken.js
Normal file
@ -0,0 +1,72 @@
|
||||
import error from '../../error'
|
||||
|
||||
module.exports = async (oauth2, client, pRefreshToken, scope) => {
|
||||
let user = null
|
||||
let ttl = null
|
||||
let refreshToken = null
|
||||
let accessToken = null
|
||||
|
||||
let resObj = {
|
||||
token_type: 'bearer'
|
||||
}
|
||||
|
||||
if (!pRefreshToken) {
|
||||
throw new error.InvalidRequest('refresh_token is mandatory for refresh_token grant type')
|
||||
}
|
||||
|
||||
try {
|
||||
refreshToken = await oauth2.model.refreshToken.fetchByToken(pRefreshToken)
|
||||
} catch (err) {
|
||||
throw new error.ServerError('Failed to call refreshToken.fetchByToken function')
|
||||
}
|
||||
|
||||
if (!refreshToken) {
|
||||
throw new error.InvalidGrant('Refresh token not found')
|
||||
}
|
||||
|
||||
if (oauth2.model.refreshToken.getClientId(refreshToken) !== oauth2.model.client.getId(client)) {
|
||||
console.warn('Client "' + oauth2.model.client.getId(client) + '" tried to fetch a refresh token which belongs to client"' +
|
||||
oauth2.model.refreshToken.getClientId(refreshToken) + '"')
|
||||
throw new error.InvalidGrant('Refresh token not found')
|
||||
}
|
||||
|
||||
try {
|
||||
user = await oauth2.model.user.fetchById(oauth2.model.refreshToken.getUserId(refreshToken))
|
||||
} catch (err) {
|
||||
throw new error.ServerError('Failed to call user.fetchById function')
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
throw new error.InvalidClient('User not found')
|
||||
}
|
||||
|
||||
try {
|
||||
accessToken = await oauth2.model.accessToken.fetchByUserIdClientId(oauth2.model.user.getId(user), oauth2.model.client.getId(client))
|
||||
} catch (err) {
|
||||
throw new error.ServerError('Failed to call accessToken.fetchByUserIdClientId function')
|
||||
}
|
||||
|
||||
if (accessToken) {
|
||||
ttl = oauth2.model.accessToken.getTTL(accessToken)
|
||||
|
||||
if (!ttl) {
|
||||
accessToken = null
|
||||
} else {
|
||||
resObj.access_token = oauth2.model.accessToken.getToken(accessToken)
|
||||
resObj.expires_in = ttl
|
||||
}
|
||||
}
|
||||
|
||||
if (!accessToken) {
|
||||
try {
|
||||
resObj.access_token = await oauth2.model.accessToken.create(oauth2.model.user.getId(user),
|
||||
oauth2.model.client.getId(client), oauth2.model.refreshToken.getScope(refreshToken), oauth2.model.accessToken.ttl)
|
||||
} catch (err) {
|
||||
throw new error.ServerError('Failed to call accessToken.create function')
|
||||
}
|
||||
|
||||
resObj.expires_in = oauth2.model.accessToken.ttl
|
||||
}
|
||||
|
||||
return resObj
|
||||
}
|
107
server/api/oauth2/error.js
Normal file
107
server/api/oauth2/error.js
Normal file
@ -0,0 +1,107 @@
|
||||
class OAuth2Error extends Error {
|
||||
constructor (code, msg, status) {
|
||||
super()
|
||||
Error.captureStackTrace(this, this.constructor)
|
||||
|
||||
this.code = code
|
||||
this.message = msg
|
||||
this.status = status
|
||||
|
||||
this.name = 'OAuth2AbstractError'
|
||||
this.logLevel = 'error'
|
||||
}
|
||||
}
|
||||
|
||||
class AccessDenied extends OAuth2Error {
|
||||
constructor (msg) {
|
||||
super('access_denied', msg, 403)
|
||||
|
||||
this.name = 'OAuth2AccessDenied'
|
||||
this.logLevel = 'info'
|
||||
}
|
||||
}
|
||||
|
||||
class InvalidClient extends OAuth2Error {
|
||||
constructor (msg) {
|
||||
super('invalid_client', msg, 401)
|
||||
|
||||
this.name = 'OAuth2InvalidClient'
|
||||
this.logLevel = 'info'
|
||||
}
|
||||
}
|
||||
|
||||
class InvalidGrant extends OAuth2Error {
|
||||
constructor (msg) {
|
||||
super('invalid_grant', msg, 400)
|
||||
|
||||
this.name = 'OAuth2InvalidGrant'
|
||||
this.logLevel = 'info'
|
||||
}
|
||||
}
|
||||
|
||||
class InvalidRequest extends OAuth2Error {
|
||||
constructor (msg) {
|
||||
super('invalid_request', msg, 400)
|
||||
|
||||
this.name = 'OAuth2InvalidRequest'
|
||||
this.logLevel = 'info'
|
||||
}
|
||||
}
|
||||
|
||||
class InvalidScope extends OAuth2Error {
|
||||
constructor (msg) {
|
||||
super('invalid_scope', msg, 400)
|
||||
|
||||
this.name = 'OAuth2InvalidScope'
|
||||
this.logLevel = 'info'
|
||||
}
|
||||
}
|
||||
|
||||
class ServerError extends OAuth2Error {
|
||||
constructor (msg) {
|
||||
super('server_error', msg, 500)
|
||||
|
||||
this.name = 'OAuth2ServerError'
|
||||
this.logLevel = 'error'
|
||||
}
|
||||
}
|
||||
|
||||
class UnauthorizedClient extends OAuth2Error {
|
||||
constructor (msg) {
|
||||
super('unauthorized_client', msg, 400)
|
||||
|
||||
this.name = 'OAuth2UnauthorizedClient'
|
||||
this.logLevel = 'info'
|
||||
}
|
||||
}
|
||||
|
||||
class UnsupportedGrantType extends OAuth2Error {
|
||||
constructor (msg) {
|
||||
super('unsupported_grant_type', msg, 400)
|
||||
|
||||
this.name = 'OAuth2UnsupportedGrantType'
|
||||
this.logLevel = 'info'
|
||||
}
|
||||
}
|
||||
|
||||
class UnsupportedResponseType extends OAuth2Error {
|
||||
constructor (msg) {
|
||||
super('unsupported_response_type', msg, 400)
|
||||
|
||||
this.name = 'OAuth2UnsupportedResponseType'
|
||||
this.logLevel = 'info'
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
OAuth2Error: OAuth2Error,
|
||||
AccessDenied: AccessDenied,
|
||||
InvalidClient: InvalidClient,
|
||||
InvalidGrant: InvalidGrant,
|
||||
InvalidRequest: InvalidRequest,
|
||||
InvalidScope: InvalidScope,
|
||||
ServerError: ServerError,
|
||||
UnauthorizedClient: UnauthorizedClient,
|
||||
UnsupportedGrantType: UnsupportedGrantType,
|
||||
UnsupportedResponseType: UnsupportedResponseType
|
||||
}
|
27
server/api/oauth2/index.js
Normal file
27
server/api/oauth2/index.js
Normal file
@ -0,0 +1,27 @@
|
||||
import middleware from './middleware'
|
||||
import controller from './controller'
|
||||
import decision from './controller/decision'
|
||||
import response from './response'
|
||||
import error from './error'
|
||||
import model from './model'
|
||||
|
||||
class OAuth2Provider {
|
||||
constructor () {
|
||||
this.bearer = middleware
|
||||
this.controller = controller
|
||||
this.decision = decision
|
||||
this.response = response
|
||||
this.error = error.OAuth2Error
|
||||
this.model = model
|
||||
}
|
||||
|
||||
express () {
|
||||
return (req, res, next) => {
|
||||
console.debug('attached')
|
||||
req.oauth2 = this
|
||||
next()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = OAuth2Provider
|
48
server/api/oauth2/middleware.js
Normal file
48
server/api/oauth2/middleware.js
Normal file
@ -0,0 +1,48 @@
|
||||
import response from './response'
|
||||
import error from './error'
|
||||
import wrap from '../../../scripts/asyncRoute'
|
||||
|
||||
const middleware = wrap(async function (req, res, next) {
|
||||
console.debug('Parsing bearer token')
|
||||
let token = null
|
||||
|
||||
// Look for token in header
|
||||
if (req.headers.authorization) {
|
||||
const pieces = req.headers.authorization.split(' ', 2)
|
||||
|
||||
// Check authorization header
|
||||
if (!pieces || pieces.length !== 2) {
|
||||
return response.error(req, res, new error.AccessDenied('Wrong authorization header'))
|
||||
}
|
||||
|
||||
// Only bearer auth is supported
|
||||
if (pieces[0].toLowerCase() !== 'bearer') {
|
||||
return response.error(req, res, new error.AccessDenied('Unsupported authorization method in header'))
|
||||
}
|
||||
|
||||
token = pieces[1]
|
||||
console.debug('Bearer token parsed from authorization header:', token)
|
||||
} else if (req.query && req.query['access_token']) {
|
||||
token = req.query['access_token']
|
||||
console.debug('Bearer token parsed from query params:', token)
|
||||
} else if (req.body && req.body['access_token']) {
|
||||
token = req.body['access_token']
|
||||
console.debug('Bearer token parsed from body params:', token)
|
||||
} else {
|
||||
return response.error(req, res, new error.AccessDenied('Bearer token not found'))
|
||||
}
|
||||
|
||||
// Try to fetch access token
|
||||
let object = await req.oauth2.model.accessToken.fetchByToken(token)
|
||||
if (!object) {
|
||||
response.error(req, res, new error.Forbidden('Token not found or has expired'))
|
||||
} else if (!req.oauth2.model.accessToken.checkTTL(object)) {
|
||||
response.error(req, res, new error.Forbidden('Token is expired'))
|
||||
} else {
|
||||
req.oauth2.accessToken = object
|
||||
console.debug('AccessToken fetched', object)
|
||||
next()
|
||||
}
|
||||
})
|
||||
|
||||
module.exports = middleware
|
258
server/api/oauth2/model.js
Normal file
258
server/api/oauth2/model.js
Normal file
@ -0,0 +1,258 @@
|
||||
import config from '../../../scripts/load-config'
|
||||
import database from '../../../scripts/load-database'
|
||||
import Models from '../models'
|
||||
import Users from '../index'
|
||||
|
||||
import crypto from 'crypto'
|
||||
|
||||
const OAuthDB = {
|
||||
accessToken: {
|
||||
ttl: config.oauth2.access_token_life,
|
||||
getToken: (object) => {
|
||||
if (object) return object.token
|
||||
return null
|
||||
},
|
||||
create: async (userId, clientId, scope, ttl) => {
|
||||
const token = crypto.randomBytes(config.oauth2.token_length).toString('hex')
|
||||
const expr = new Date(Date.now() + ttl * 1000)
|
||||
|
||||
if (typeof scope === 'object') {
|
||||
scope = scope.join(' ')
|
||||
}
|
||||
|
||||
// Delete already existing tokens with this exact user id, client id and scope, because it will
|
||||
// eventually pile up and flood the database.
|
||||
await Models.OAuth2AccessToken.query().delete().where('user_id', userId)
|
||||
.andWhere('client_id', clientId)
|
||||
|
||||
const obj = { token: token, user_id: userId, client_id: clientId, scope: scope, expires_at: expr, created_at: new Date() }
|
||||
|
||||
let res = await Models.OAuth2AccessToken.query().insert(obj)
|
||||
if (!res) return null
|
||||
|
||||
return res.token
|
||||
},
|
||||
fetchByToken: async (token) => {
|
||||
if (typeof token === 'object') {
|
||||
return token
|
||||
}
|
||||
|
||||
token = await Models.OAuth2AccessToken.query().where('token', token)
|
||||
if (!token.length) return null
|
||||
|
||||
return token[0]
|
||||
},
|
||||
checkTTL: (object) => {
|
||||
return (object.expires_at > Date.now())
|
||||
},
|
||||
getTTL: (object) => {
|
||||
return (object.expires_at - Date.now())
|
||||
},
|
||||
fetchByUserIdClientId: async (userId, clientId) => {
|
||||
let tkn = await Models.OAuth2AccessToken.query().where('user_id', userId).andWhere('client_id', clientId)
|
||||
|
||||
if (!tkn.length) return null
|
||||
|
||||
return tkn[0]
|
||||
}
|
||||
},
|
||||
client: {
|
||||
getId: (client) => {
|
||||
return client.id
|
||||
},
|
||||
fetchById: async (id) => {
|
||||
let client = await Models.OAuth2Client.query().where('id', id)
|
||||
|
||||
if (!client.length) return null
|
||||
|
||||
return client[0]
|
||||
},
|
||||
checkSecret: (client, secret) => {
|
||||
return client.secret === secret
|
||||
},
|
||||
checkGrantType: (client, grant) => {
|
||||
if (client.grants.indexOf(grant) !== -1) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
},
|
||||
getRedirectUri: (client) => {
|
||||
return client.redirect_url
|
||||
},
|
||||
checkRedirectUri: (client, redirectUri) => {
|
||||
return (redirectUri.indexOf(OAuthDB.client.getRedirectUri(client)) === 0 &&
|
||||
redirectUri.replace(OAuthDB.client.getRedirectUri(client), '').indexOf('#') === -1)
|
||||
},
|
||||
transformScope: (scope) => {
|
||||
if (!scope) return []
|
||||
if (typeof scope === 'object') {
|
||||
return scope
|
||||
}
|
||||
|
||||
scope = scope.trim()
|
||||
if (scope.indexOf(',') != -1) {
|
||||
scope = scope.split(',')
|
||||
} else {
|
||||
scope = scope.split(' ')
|
||||
}
|
||||
|
||||
return scope
|
||||
},
|
||||
checkScope: (client, scope) => {
|
||||
if (!scope) return []
|
||||
if (typeof scope === 'string') {
|
||||
scope = OAuthDB.client.transformScope(scope)
|
||||
}
|
||||
|
||||
let clientScopes = client.scope.split(' ')
|
||||
|
||||
for (let i in scope) {
|
||||
if (clientScopes.indexOf(scope[i]) === -1) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return scope
|
||||
}
|
||||
},
|
||||
code: {
|
||||
ttl: config.oauth2.code_life,
|
||||
create: async (userId, clientId, scope, ttl) => {
|
||||
const code = crypto.randomBytes(config.oauth2.token_length).toString('hex')
|
||||
const expr = new Date(Date.now() + ttl * 1000)
|
||||
|
||||
if (typeof scope === 'object') {
|
||||
scope = scope.join(' ')
|
||||
}
|
||||
|
||||
// Delete already existing codes with this exact user id, client id and scope, because it will
|
||||
// eventually pile up and flood the database, especially when they were never used.
|
||||
await Models.OAuth2Code.query().delete().where('user_id', userId).andWhere('client_id', clientId)
|
||||
|
||||
const obj = { code: code, user_id: userId, client_id: clientId, scope: scope, expires_at: expr, created_at: new Date() }
|
||||
console.log(obj)
|
||||
|
||||
await Models.OAuth2Code.query().insert(obj)
|
||||
|
||||
return obj.code
|
||||
},
|
||||
fetchByCode: async (code) => {
|
||||
code = await Models.OAuth2Code.query().where('code', code)
|
||||
|
||||
if (!code.length) return null
|
||||
|
||||
return code[0]
|
||||
},
|
||||
removeByCode: async (code) => {
|
||||
if (typeof code === 'object') {
|
||||
code = code.code
|
||||
}
|
||||
|
||||
return await Models.OAuth2Code.query().delete().where('code', code)
|
||||
},
|
||||
getUserId: (code) => {
|
||||
return code.user_id
|
||||
},
|
||||
getClientId: (code) => {
|
||||
return code.client_id
|
||||
},
|
||||
getScope: (code) => {
|
||||
return code.scope
|
||||
},
|
||||
checkTTL: (code) => {
|
||||
return (code.expires_at > Date.now())
|
||||
}
|
||||
},
|
||||
refreshToken: {
|
||||
create: async (userId, clientId, scope) => {
|
||||
const token = crypto.randomBytes(config.oauth2.token_length).toString('hex')
|
||||
|
||||
if (typeof scope === 'object') {
|
||||
scope = scope.join(' ')
|
||||
}
|
||||
|
||||
const obj = { token: token, user_id: userId, client_id: clientId, scope: scope, created_at: new Date() }
|
||||
|
||||
await Models.OAuth2RefreshToken.query().insert(obj)
|
||||
|
||||
return obj.token
|
||||
},
|
||||
fetchByToken: async (token) => {
|
||||
token = await Models.OAuth2RefreshToken.query().where('token', token)
|
||||
|
||||
if (!token.length) return null
|
||||
|
||||
return token[0]
|
||||
},
|
||||
removeByUserIdClientId: async (userId, clientId) => {
|
||||
return await Models.OAuth2RefreshToken.query().delete().where('user_id', userId)
|
||||
.andWhere('client_id', clientId)
|
||||
},
|
||||
removeByRefreshToken: async (token) => {
|
||||
return await Models.OAuth2RefreshToken.query().delete().where('token', token)
|
||||
},
|
||||
getUserId: (refreshToken) => {
|
||||
return refreshToken.user_id
|
||||
},
|
||||
getClientId: (refreshToken) => {
|
||||
return refreshToken.client_id
|
||||
},
|
||||
getScope: (refreshToken) => {
|
||||
return refreshToken.scope
|
||||
}
|
||||
},
|
||||
user: {
|
||||
getId: (user) => {
|
||||
return user.id
|
||||
},
|
||||
fetchById: Users.User.get,
|
||||
fetchByUsername: Users.User.get,
|
||||
checkPassword: Users.User.Login.password,
|
||||
fetchFromRequest: async (req) => {
|
||||
if (!req.session.user) return null
|
||||
return req.session.user
|
||||
},
|
||||
clientAllowed: async (userId, clientId, scope) => {
|
||||
if (typeof scope === 'object') {
|
||||
scope = scope.join(' ')
|
||||
}
|
||||
|
||||
let authorized = await Models.OAuth2AuthorizedClient.query().where('user_id', userId)
|
||||
if (!authorized.length) return false
|
||||
|
||||
let correct = false
|
||||
for (let i in authorized) {
|
||||
if (authorized[i].client_id === clientId) {
|
||||
correct = authorized[i]
|
||||
}
|
||||
}
|
||||
|
||||
if (correct) {
|
||||
if (correct.scope !== scope) {
|
||||
await Models.OAuth2AuthorizedClient.query().delete().where('user_id', userId)
|
||||
.andWhere('client_id', correct.client_id)
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
correct = true
|
||||
}
|
||||
|
||||
return correct
|
||||
},
|
||||
allowClient: async (userId, clientId, scope) => {
|
||||
if (typeof scope === 'object') {
|
||||
scope = scope.join(' ')
|
||||
}
|
||||
|
||||
let obj = { user_id: userId, client_id: clientId, scope: scope, created_at: new Date() }
|
||||
|
||||
await Models.OAuth2AuthorizedClient.query().insert(obj)
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = OAuthDB
|
60
server/api/oauth2/response.js
Normal file
60
server/api/oauth2/response.js
Normal file
@ -0,0 +1,60 @@
|
||||
import query from 'querystring'
|
||||
import error from './error'
|
||||
|
||||
function data (req, res, code, data) {
|
||||
res.header('Cache-Control', 'no-store')
|
||||
res.header('Pragma', 'no-cache')
|
||||
res.status(code).send(data)
|
||||
console.debug('Response: ', data)
|
||||
}
|
||||
|
||||
function redirect (req, res, redirectUri) {
|
||||
res.header('Location', redirectUri)
|
||||
res.status(302).end()
|
||||
console.debug('Redirecting to ', redirectUri)
|
||||
}
|
||||
|
||||
module.exports.error = function (req, res, err, redirectUri) {
|
||||
// Transform unknown error
|
||||
if (!(err instanceof error.OAuth2Error)) {
|
||||
console.error(err.stack)
|
||||
err = new error.ServerError('Uncaught exception')
|
||||
} else {
|
||||
console.error('Exception caught', err.stack)
|
||||
}
|
||||
|
||||
if (redirectUri) {
|
||||
let obj = {
|
||||
error: err.code,
|
||||
error_description: err.message
|
||||
}
|
||||
|
||||
if (req.query.state) {
|
||||
obj.state = req.query.state
|
||||
}
|
||||
|
||||
redirectUri += '?' + query.stringify(obj)
|
||||
redirect(req, res, redirectUri)
|
||||
} else {
|
||||
data(req, res, err.status, {error: err.code, error_description: err.message})
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.data = function (req, res, obj, redirectUri, anchor) {
|
||||
if (redirectUri) {
|
||||
if (anchor) {
|
||||
redirectUri += '#'
|
||||
} else {
|
||||
redirectUri += (redirectUri.indexOf('?') === -1 ? '?' : '&')
|
||||
}
|
||||
|
||||
if (req.query.state) {
|
||||
obj.state = req.query.state
|
||||
}
|
||||
|
||||
redirectUri += query.stringify(obj)
|
||||
redirect(req, res, redirectUri)
|
||||
} else {
|
||||
data(req, res, 200, obj)
|
||||
}
|
||||
}
|
@ -7,6 +7,7 @@ import wrap from '../../scripts/asyncRoute'
|
||||
import API from '../api'
|
||||
|
||||
import apiRouter from './api'
|
||||
import oauthRouter from './oauth2'
|
||||
|
||||
let router = express.Router()
|
||||
|
||||
@ -22,6 +23,8 @@ router.use(wrap(async (req, res, next) => {
|
||||
next()
|
||||
}))
|
||||
|
||||
router.use('/oauth2', oauthRouter)
|
||||
|
||||
/*
|
||||
================
|
||||
RENDER VIEWS
|
||||
@ -114,6 +117,14 @@ router.get('/login/verify', wrap(async (req, res) => {
|
||||
*/
|
||||
|
||||
function formError (req, res, error, redirect) {
|
||||
// Security measures
|
||||
if (req.body.password) {
|
||||
delete req.body.password
|
||||
if (req.body.password_repeat) {
|
||||
delete req.body.password_repeat
|
||||
}
|
||||
}
|
||||
|
||||
req.flash('formkeep', req.body || {})
|
||||
req.flash('message', {error: true, text: error})
|
||||
res.redirect(redirect || parseurl(req).path)
|
||||
|
59
server/routes/oauth2.js
Normal file
59
server/routes/oauth2.js
Normal file
@ -0,0 +1,59 @@
|
||||
import express from 'express'
|
||||
import uapi from '../api'
|
||||
import OAuth2 from '../api/oauth2'
|
||||
import config from '../../scripts/load-config'
|
||||
import wrap from '../../scripts/asyncRoute'
|
||||
|
||||
let router = express.Router()
|
||||
let oauth = new OAuth2()
|
||||
|
||||
router.use(oauth.express())
|
||||
|
||||
function ensureLoggedIn (req, res, next) {
|
||||
if (req.session.user) {
|
||||
next()
|
||||
} else {
|
||||
req.session.redirectUri = req.originalUrl
|
||||
res.redirect('/login')
|
||||
}
|
||||
}
|
||||
|
||||
router.use('/authorize', ensureLoggedIn, oauth.controller.authorization)
|
||||
router.post('/token', oauth.controller.token)
|
||||
router.post('/introspect', oauth.controller.introspection)
|
||||
|
||||
router.get('/user', oauth.bearer, wrap(async (req, res) => {
|
||||
let accessToken = req.oauth2.accessToken
|
||||
let user = await uapi.User.get(accessToken.user_id)
|
||||
if (!user) {
|
||||
return res.status(404).jsonp({
|
||||
error: 'No such user'
|
||||
})
|
||||
}
|
||||
|
||||
let udata = {
|
||||
id: user.id,
|
||||
name: user.display_name,
|
||||
avatar_file: user.avatar_file
|
||||
}
|
||||
|
||||
if (accessToken.scope.indexOf('email') != -1) {
|
||||
udata.email = user.email
|
||||
}
|
||||
|
||||
if (accessToken.scope.indexOf('privilege') != -1) {
|
||||
udata.privilege = user.nw_privilege
|
||||
}
|
||||
|
||||
res.jsonp(udata)
|
||||
}))
|
||||
|
||||
router.use((err, req, res, next) => {
|
||||
if (err && err instanceof oauth.error) {
|
||||
return oauth.response.error(req, res, err, req.body.redirectUri)
|
||||
}
|
||||
|
||||
next()
|
||||
})
|
||||
|
||||
module.exports = router
|
@ -22,6 +22,11 @@ process.once('message', (args) => {
|
||||
process.send(util.format.apply(this, arguments))
|
||||
}
|
||||
|
||||
console.debug = function () {
|
||||
if (!args.dev) return
|
||||
process.send('[DEBUG] ' + util.format.apply(this, arguments))
|
||||
}
|
||||
|
||||
console.warn = function () {
|
||||
process.send('warn ' + util.format.apply(this, arguments))
|
||||
}
|
||||
|
@ -83,15 +83,49 @@ a
|
||||
section
|
||||
font-family: "Open Sans"
|
||||
position: relative
|
||||
height: 100vh
|
||||
min-height: 100vh
|
||||
.content
|
||||
position: absolute
|
||||
left: 0
|
||||
right: 0
|
||||
top: 0
|
||||
bottom: 40%
|
||||
overflow: auto
|
||||
background-color: #ffffff
|
||||
padding: 40px
|
||||
padding: 20px
|
||||
&#home
|
||||
h1
|
||||
font-size: 300%
|
||||
text-align: center
|
||||
color: green
|
||||
margin-bottom: 0
|
||||
p
|
||||
text-align: center;
|
||||
font-size: 200%
|
||||
color: #0074d0;
|
||||
font-weight: bold;
|
||||
|
||||
.divboxes
|
||||
text-align: center
|
||||
.divbox
|
||||
width: 300px
|
||||
display: inline-block
|
||||
margin: 20px
|
||||
vertical-align: text-top
|
||||
.head
|
||||
display: block
|
||||
font-size: 30px
|
||||
.fa
|
||||
color: white
|
||||
height: 32px
|
||||
width: 32px
|
||||
padding: 10px
|
||||
border-radius: 100px
|
||||
margin-right: 15px
|
||||
.fa-lock
|
||||
background-color: orange
|
||||
.fa-github
|
||||
background-color: green
|
||||
|
||||
footer
|
||||
padding: 20px
|
||||
@ -157,6 +191,8 @@ input[type="submit"]
|
||||
.left, .right
|
||||
display: inline-block
|
||||
width: 50%
|
||||
.left
|
||||
border-right: 1px solid #ddd
|
||||
.right
|
||||
float: right
|
||||
width: 46%
|
||||
@ -224,6 +260,63 @@ span.divider
|
||||
margin: 0 5px
|
||||
cursor: default
|
||||
|
||||
.nobulletin
|
||||
list-style-type: none
|
||||
padding-left: 2em
|
||||
i.fa
|
||||
margin-right: 10px
|
||||
|
||||
.haveaccess
|
||||
color: #0c9a0c
|
||||
span
|
||||
font-weight: bold
|
||||
.noaccess
|
||||
color: red
|
||||
span
|
||||
font-weight: bold
|
||||
|
||||
.unaffiliated
|
||||
font-size: 120%
|
||||
text-align: center
|
||||
color: #FF5722
|
||||
font-weight: bold
|
||||
|
||||
.application
|
||||
height: 140px
|
||||
.picture
|
||||
width: 120px
|
||||
height: 120px
|
||||
position: relative
|
||||
display: inline-block
|
||||
float: left
|
||||
.noicon
|
||||
width: 100%
|
||||
height: 100%
|
||||
text-align: center
|
||||
background-color: #40aaff
|
||||
color: #fff
|
||||
i
|
||||
font-size: 500%
|
||||
margin-top: 20px
|
||||
.info
|
||||
margin-left: 130px
|
||||
.name
|
||||
font-size: 150%
|
||||
font-weight: bold
|
||||
.description
|
||||
font-size: 80%
|
||||
font-style: italic
|
||||
.url
|
||||
display: block
|
||||
text-overflow: ellipsis
|
||||
overflow: hidden
|
||||
|
||||
input.authorize
|
||||
background-color: #00b0ff
|
||||
font-size: 200%
|
||||
color: #fff
|
||||
border: 1px solid #2196F3
|
||||
|
||||
@media all and (max-width: 800px)
|
||||
.navigator
|
||||
padding: 0 10px
|
||||
@ -245,7 +338,29 @@ span.divider
|
||||
display: block
|
||||
.copyright
|
||||
margin-left: 0
|
||||
.document
|
||||
padding: 5px
|
||||
.tos
|
||||
width: auto
|
||||
font-size: 4vw
|
||||
.divbox
|
||||
font-size: 80%
|
||||
.head
|
||||
font-size: 120%
|
||||
.fa
|
||||
width: 15px
|
||||
height: 15px
|
||||
section#home
|
||||
h1
|
||||
font-size: 150%
|
||||
margin-top: 0
|
||||
p
|
||||
font-size: 100%
|
||||
|
||||
@media all and (max-width: 500px)
|
||||
section
|
||||
padding: 10px
|
||||
.logo.small
|
||||
font-size: 5vw
|
||||
.divbox
|
||||
margin: 10px 0px
|
||||
|
61
views/authorization.pug
Normal file
61
views/authorization.pug
Normal file
@ -0,0 +1,61 @@
|
||||
extends layout
|
||||
block title
|
||||
|Icy Network - Authorize Client
|
||||
|
||||
block body
|
||||
.wrapper
|
||||
.boxcont
|
||||
.box#login
|
||||
h1 Authorize OAuth2 Application
|
||||
.left
|
||||
.application
|
||||
.picture
|
||||
if client.icon
|
||||
img(src=client.icon)
|
||||
else
|
||||
.noicon
|
||||
i.fa.fa-fw.fa-gears
|
||||
.info
|
||||
.name= client.title
|
||||
.description= client.description
|
||||
a.url(href=client.url)= client.url
|
||||
form#loginForm(method="POST", action="")
|
||||
input(type="hidden", name="csrf", value=csrf)
|
||||
input(type="hidden", name="decision", value='1')
|
||||
input.authorize(type="submit", value="Authorize")
|
||||
form#loginForm(method="POST", action="")
|
||||
input(type="hidden", name="csrf", value=csrf)
|
||||
input(type="hidden", name="decision", value='0')
|
||||
input(type="submit", value="Deny")
|
||||
.right
|
||||
.haveaccess
|
||||
span This application can
|
||||
ul.nobulletin
|
||||
if scope.indexOf('email') !== -1
|
||||
li
|
||||
i.fa.fa-fw.fa-envelope
|
||||
|See your Email address
|
||||
li
|
||||
i.fa.fa-fw.fa-user
|
||||
|See your Display Name
|
||||
.noaccess
|
||||
span This application cannot
|
||||
ul.nobulletin
|
||||
if scope.indexOf('email') === -1
|
||||
li
|
||||
i.fa.fa-fw.fa-envelope
|
||||
|See your Email address
|
||||
li
|
||||
i.fa.fa-fw.fa-lock
|
||||
|See your Password
|
||||
li
|
||||
i.fa.fa-fw.fa-gears
|
||||
|Change your Account Settings
|
||||
if client.verified != 1
|
||||
.unaffiliated
|
||||
br
|
||||
span
|
||||
i.fa.fa-fw.fa-warning
|
||||
|This application is not affiliated with Icy Network
|
||||
i.fa.fa-fw.fa-warning
|
||||
|
@ -6,6 +6,19 @@ block body
|
||||
section#home
|
||||
.content
|
||||
h1 Welcome to Icy Network!
|
||||
p Icy Network is a Global Network of Communities and Websites, United by a Single Login
|
||||
.divboxes
|
||||
.divbox#secure
|
||||
span.head
|
||||
i.fa.fa-lock.fa-fw
|
||||
span.text Secure Login
|
||||
span.text A secure login system with Two-Factor Authentication possibility
|
||||
.divbox#secure
|
||||
span.head
|
||||
i.fa.fa-github.fa-fw
|
||||
span.text Open Source
|
||||
span.text All of our platforms are Open Source Software hosted on our
|
||||
a(href="https://github.com/IcyNet", target="_blank") GitHub Organization
|
||||
section#news
|
||||
.content
|
||||
h1 Icy Network News
|
||||
|
@ -17,11 +17,11 @@ block body
|
||||
form#loginForm(method="POST", action="")
|
||||
input(type="hidden", name="csrf", value=csrf)
|
||||
label(for="username") Username
|
||||
input(type="text", name="username", id="username")
|
||||
input(type="text", name="username", id="username", value=formkeep.username)
|
||||
label(for="display_name") Display Name
|
||||
input(type="text", name="display_name", id="display_name")
|
||||
input(type="text", name="display_name", id="display_name", value=formkeep.display_name)
|
||||
label(for="email") Email Address
|
||||
input(type="email", name="email", id="email")
|
||||
input(type="email", name="email", id="email", value=formkeep.email)
|
||||
label(for="password") Password
|
||||
input(type="password", name="password", id="password")
|
||||
label(for="password_repeat") Repeat Password
|
||||
|
Reference in New Issue
Block a user