feat(server/frontend): auth revamp WIP
This commit is contained in:
@@ -42,7 +42,6 @@ exports.init = async ( ) => {
|
||||
}
|
||||
|
||||
if ( process.env.COMPRESSION ) {
|
||||
debug( `speckle:startup` )( 'Using app level compression. Consider enabling this at a proxy level.' )
|
||||
app.use( compression( ) )
|
||||
}
|
||||
|
||||
@@ -77,7 +76,9 @@ exports.init = async ( ) => {
|
||||
plugins: [
|
||||
require( `${appRoot}/logging/apolloPlugin` )
|
||||
],
|
||||
tracing: process.env.NODE_ENV === 'development'
|
||||
tracing: process.env.NODE_ENV === 'development',
|
||||
introspection: true,
|
||||
playground: true
|
||||
} )
|
||||
|
||||
graphqlServer.applyMiddleware( { app: app } )
|
||||
@@ -106,8 +107,7 @@ exports.startHttp = async ( app ) => {
|
||||
|
||||
debug( 'speckle:http-startup' )( '✨ Proxying frontend (dev mode):' )
|
||||
debug( 'speckle:http-startup' )( `👉 main application: http://localhost:${port}/` )
|
||||
debug( 'speckle:http-startup' )( `👉 auth application: http://localhost:${port}/auth` )
|
||||
debug( 'speckle:hint' )( ` ℹ️ Don't forget to run "npm run dev:frontend" in a different terminal to start the vue application.` )
|
||||
debug( 'speckle:hint' )( 'ℹ️ Don\'t forget to run "npm run dev:frontend" in a different terminal to start the vue application.' )
|
||||
}
|
||||
|
||||
// Production mode -> serve things statically.
|
||||
@@ -136,7 +136,7 @@ exports.startHttp = async ( app ) => {
|
||||
app.use( Sentry.Handlers.errorHandler( ) )
|
||||
|
||||
server.on( 'listening', ( ) => {
|
||||
debug( `speckle:startup` )( ` 🚀 My name is Spockle Server, and I'm running at ${server.address().port}` )
|
||||
debug( 'speckle:startup' )( ` 🚀 My name is Spockle Server, and I'm running at ${server.address().port}` )
|
||||
} )
|
||||
|
||||
server.listen( port )
|
||||
|
||||
@@ -13,7 +13,6 @@ export async function checkAccessCodeAndGetTokens() {
|
||||
let response = await getTokenFromAccessCode(accessCode)
|
||||
// eslint-disable-next-line no-prototype-builtins
|
||||
if (response.hasOwnProperty('token')) {
|
||||
localStorage.clear()
|
||||
localStorage.setItem('AuthToken', response.token)
|
||||
localStorage.setItem('RefreshToken', response.refreshToken)
|
||||
window.history.replaceState({}, document.title, '/')
|
||||
@@ -68,14 +67,6 @@ export async function getTokenFromAccessCode(accessCode) {
|
||||
return data
|
||||
}
|
||||
|
||||
export function redirectToAuth() {
|
||||
// Reaching this stage means we're initialising a full new auth flow,
|
||||
// TIP: also means we need to refresh the app challenge as well.
|
||||
localStorage.setItem('appChallenge', crs({ length: 10 }))
|
||||
// Finally, redirect to the auth lock.
|
||||
window.location = `/auth?appId=${appId}&challenge=${localStorage.getItem('appChallenge')}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Signs out the current session
|
||||
* @return {null}
|
||||
|
||||
@@ -161,38 +161,23 @@ const router = new VueRouter({
|
||||
router.beforeEach((to, from, next) => {
|
||||
let uuid = localStorage.getItem('uuid')
|
||||
let redirect = localStorage.getItem('shouldRedirectTo')
|
||||
let path = to.path
|
||||
let name = to.name
|
||||
|
||||
console.log(uuid, redirect, path, name)
|
||||
if (!uuid && to.name !== 'Login' && to.name !== 'Register') {
|
||||
localStorage.setItem('shouldRedirectTo', to.path)
|
||||
|
||||
if (!uuid && (to.name !== 'Login' && to.name !== 'Register')) return next({ name: 'Login' })
|
||||
return next({ name: 'Login' })
|
||||
}
|
||||
|
||||
if ((to.name === 'Login' || to.name === 'Register') && uuid) {
|
||||
return next({ name: 'home' })
|
||||
}
|
||||
|
||||
if (uuid && redirect && redirect !== to.path) {
|
||||
localStorage.removeItem('shouldRedirectTo')
|
||||
return next({ path: redirect })
|
||||
}
|
||||
|
||||
return next()
|
||||
// if (!(to.name === 'Login' || to.name === 'Register') && !localStorage.getItem('uuid')) {
|
||||
// localStorage.setItem('shouldRedirectTo', to.path)
|
||||
// return next({ name: 'Login' })
|
||||
// } else if (
|
||||
// (to.name === 'Login' || to.name === 'Register') &&
|
||||
// !!localStorage.getItem('uuid') &&
|
||||
// !!localStorage.getItem('shouldRedirectTo')
|
||||
// ) {
|
||||
// return next({ name: 'home' })
|
||||
// } else if (localStorage.getItem('shouldRedirectTo')) {
|
||||
// let path = localStorage.getItem('shouldRedirectTo')
|
||||
// localStorage.removeItem('shouldRedirectTo')
|
||||
// return next({ path })
|
||||
// } else {
|
||||
// return next()
|
||||
// }
|
||||
|
||||
// else if (localStorage.getItem('uuid')) {
|
||||
// let shouldRedirectTo = localStorage.getItem('shouldRedirectTo')
|
||||
// localStorage.removeItem('shouldRedirectTo')
|
||||
// if (shouldRedirectTo) {
|
||||
// return next() // TODO: redirect to prev url
|
||||
// } else return next()
|
||||
// }
|
||||
})
|
||||
|
||||
//TODO: include stream name in page title eg `My Cool Stream | Speckle`
|
||||
|
||||
+114
-114
@@ -25,127 +25,127 @@
|
||||
<script crossorigin src="https://unpkg.com/graphiql/graphiql.min.js"></script>
|
||||
<script>
|
||||
const graphQLFetcher = graphQLParams =>
|
||||
fetch( '/graphql', {
|
||||
method: 'post',
|
||||
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + localStorage.getItem( 'AuthToken' ) },
|
||||
body: JSON.stringify( graphQLParams ),
|
||||
} )
|
||||
fetch( '/graphql', {
|
||||
method: 'post',
|
||||
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + localStorage.getItem( 'AuthToken' ) },
|
||||
body: JSON.stringify( graphQLParams ),
|
||||
} )
|
||||
.then( response => response.json( ) )
|
||||
.catch( ( ) => response.text( ) )
|
||||
|
||||
|
||||
function redirectToAuth( ) {
|
||||
localStorage.setItem( 'challenge', Math.random( ).toString( 36 ).substring( 2, 15 ) + Math.random( ).toString( 36 ).substring( 2, 15 ) )
|
||||
window.location = '/auth?appId=explorer&challenge=' + localStorage.getItem( 'challenge' )
|
||||
}
|
||||
function redirectToAuth( ) {
|
||||
localStorage.setItem( 'challenge', Math.random( ).toString( 36 ).substring( 2, 15 ) + Math.random( ).toString( 36 ).substring( 2, 15 ) )
|
||||
window.location = `/authn/verify/explorer/${localStorage.getItem( 'challenge' )}`
|
||||
}
|
||||
|
||||
async function accessCodeExchange( accessCode ) {
|
||||
window.history.replaceState( {}, document.title, '/explorer' )
|
||||
async function accessCodeExchange( accessCode ) {
|
||||
window.history.replaceState( {}, document.title, '/explorer' )
|
||||
|
||||
let response = await fetch( '/auth/token', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify( {
|
||||
accessCode: accessCode,
|
||||
appId: 'explorer',
|
||||
appSecret: 'explorer',
|
||||
challenge: localStorage.getItem( 'challenge' )
|
||||
} )
|
||||
let response = await fetch( '/auth/token', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify( {
|
||||
accessCode: accessCode,
|
||||
appId: 'explorer',
|
||||
appSecret: 'explorer',
|
||||
challenge: localStorage.getItem( 'challenge' )
|
||||
} )
|
||||
|
||||
let data = await response.json( )
|
||||
|
||||
if ( data.hasOwnProperty( 'token' ) ) {
|
||||
localStorage.removeItem( 'challenge' )
|
||||
localStorage.setItem( 'AuthToken', data.token )
|
||||
localStorage.setItem( 'RefreshToken', data.refreshToken )
|
||||
await setUserName( )
|
||||
await initGQL( data.token )
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async function initGQL( token ) {
|
||||
// GraphQLPlayground.init( document.getElementById( 'root' ), {
|
||||
// endpointUrl: '/graphql',
|
||||
// headers: {
|
||||
// 'Authorization': 'Bearer ' + token
|
||||
// }
|
||||
// } )
|
||||
ReactDOM.render(
|
||||
React.createElement( GraphiQL, { fetcher: graphQLFetcher } ),
|
||||
document.getElementById( 'graphiql' ),
|
||||
)
|
||||
await setServerInfo( )
|
||||
}
|
||||
|
||||
async function setUserName( ) {
|
||||
let testResponse = await fetch( '/graphql', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + localStorage.getItem( 'AuthToken' ),
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify( { query: `{ user { name } }` } )
|
||||
} )
|
||||
|
||||
let data = ( await testResponse.json( ) ).data
|
||||
document.getElementById( 'username' ).innerHTML = data.user.name
|
||||
}
|
||||
|
||||
async function setServerInfo( ) {
|
||||
let testResponse = await fetch( '/graphql', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify( { query: `{ serverInfo { name company } }` } )
|
||||
} )
|
||||
|
||||
let data = ( await testResponse.json( ) ).data
|
||||
document.getElementById( 'serverDetails' ).innerHTML = `<b>${data.serverInfo.name}</b> deployed by <b>${data.serverInfo.company}</b>`
|
||||
}
|
||||
|
||||
function logout( ) {
|
||||
localStorage.removeItem( 'AuthToken' )
|
||||
localStorage.removeItem( 'RefreshToken' )
|
||||
window.location = '/explorer'
|
||||
}
|
||||
|
||||
window.addEventListener( 'load', async function ( event ) {
|
||||
let urlParams = new URLSearchParams( window.location.search )
|
||||
let accessCode = urlParams.get( 'access_code' )
|
||||
|
||||
if ( accessCode ) {
|
||||
accessCodeExchange( accessCode )
|
||||
} else {
|
||||
let token = localStorage.getItem( 'AuthToken' )
|
||||
if ( token ) {
|
||||
let testResponse = await fetch( '/graphql', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + token,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify( { query: `{ user { name } }` } )
|
||||
} )
|
||||
|
||||
let data = ( await testResponse.json( ) ).data
|
||||
// if res.data.user is non null, means the ping was ok & token is valid
|
||||
if ( data.user ) {
|
||||
console.log( data.user )
|
||||
document.getElementById( 'username' ).innerHTML = data.user.name
|
||||
await initGQL( token )
|
||||
|
||||
}
|
||||
} else {
|
||||
redirectToAuth( )
|
||||
}
|
||||
}
|
||||
} )
|
||||
</script>
|
||||
|
||||
let data = await response.json( )
|
||||
|
||||
if ( data.hasOwnProperty( 'token' ) ) {
|
||||
localStorage.removeItem( 'challenge' )
|
||||
localStorage.setItem( 'AuthToken', data.token )
|
||||
localStorage.setItem( 'RefreshToken', data.refreshToken )
|
||||
await setUserName( )
|
||||
await initGQL( data.token )
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async function initGQL( token ) {
|
||||
// GraphQLPlayground.init( document.getElementById( 'root' ), {
|
||||
// endpointUrl: '/graphql',
|
||||
// headers: {
|
||||
// 'Authorization': 'Bearer ' + token
|
||||
// }
|
||||
// } )
|
||||
ReactDOM.render(
|
||||
React.createElement( GraphiQL, { fetcher: graphQLFetcher } ),
|
||||
document.getElementById( 'graphiql' ),
|
||||
)
|
||||
await setServerInfo( )
|
||||
}
|
||||
|
||||
async function setUserName( ) {
|
||||
let testResponse = await fetch( '/graphql', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + localStorage.getItem( 'AuthToken' ),
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify( { query: `{ user { name } }` } )
|
||||
} )
|
||||
|
||||
let data = ( await testResponse.json( ) ).data
|
||||
document.getElementById( 'username' ).innerHTML = data.user.name
|
||||
}
|
||||
|
||||
async function setServerInfo( ) {
|
||||
let testResponse = await fetch( '/graphql', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify( { query: `{ serverInfo { name company } }` } )
|
||||
} )
|
||||
|
||||
let data = ( await testResponse.json( ) ).data
|
||||
document.getElementById( 'serverDetails' ).innerHTML = `<b>${data.serverInfo.name}</b> deployed by <b>${data.serverInfo.company}</b>`
|
||||
}
|
||||
|
||||
function logout( ) {
|
||||
localStorage.removeItem( 'AuthToken' )
|
||||
localStorage.removeItem( 'RefreshToken' )
|
||||
window.location = '/explorer'
|
||||
}
|
||||
|
||||
window.addEventListener( 'load', async function ( event ) {
|
||||
let urlParams = new URLSearchParams( window.location.search )
|
||||
let accessCode = urlParams.get( 'access_code' )
|
||||
|
||||
if ( accessCode ) {
|
||||
accessCodeExchange( accessCode )
|
||||
} else {
|
||||
let token = localStorage.getItem( 'AuthToken' )
|
||||
if ( token ) {
|
||||
let testResponse = await fetch( '/graphql', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + token,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify( { query: '{ user { name } }' } )
|
||||
} )
|
||||
|
||||
let data = ( await testResponse.json( ) ).data
|
||||
// if res.data.user is non null, means the ping was ok & token is valid
|
||||
if ( data.user ) {
|
||||
console.log( data.user )
|
||||
document.getElementById( 'username' ).innerHTML = data.user.name
|
||||
await initGQL( token )
|
||||
|
||||
}
|
||||
} else {
|
||||
redirectToAuth( )
|
||||
}
|
||||
}
|
||||
} )
|
||||
</script>
|
||||
</body>
|
||||
<style>
|
||||
#speckle-header {
|
||||
@@ -437,4 +437,4 @@ li.CodeMirror-hint-active {
|
||||
}
|
||||
</style>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -38,6 +38,7 @@ type ServerAppListItem {
|
||||
type AppAuthor {
|
||||
name: String
|
||||
id: String
|
||||
avatar: String
|
||||
}
|
||||
|
||||
extend type User {
|
||||
|
||||
+95
-68
@@ -8,8 +8,10 @@ const passport = require( 'passport' )
|
||||
const debug = require( 'debug' )
|
||||
|
||||
const sentry = require( `${appRoot}/logging/sentryHelper` )
|
||||
const { getApp, createAuthorizationCode, createAppTokenFromAccessCode, refreshAppToken } = require( './services/apps' )
|
||||
const { createPersonalAccessToken } = require( `${appRoot}/modules/core/services/tokens` )
|
||||
const { getApp, getAllAppsAuthorizedByUser, createAuthorizationCode, createAppTokenFromAccessCode, refreshAppToken } = require( './services/apps' )
|
||||
const { createPersonalAccessToken, validateToken, revokeTokenById } = require( `${appRoot}/modules/core/services/tokens` )
|
||||
const { revokeRefreshToken } = require( `${appRoot}/modules/auth/services/apps` )
|
||||
const { validateScopes, contextMiddleware } = require( `${appRoot}/modules/shared` )
|
||||
|
||||
let authStrategies = [ ]
|
||||
|
||||
@@ -31,61 +33,95 @@ exports.init = ( app, options ) => {
|
||||
cookie: { maxAge: 1000 * 60 * 3 } // 3 minutes
|
||||
} )
|
||||
|
||||
let sessionAppId = ( req, res, next ) => {
|
||||
|
||||
req.session.appId = req.query.appId
|
||||
let sessionStorage = ( req, res, next ) => {
|
||||
req.session.challenge = req.query.challenge
|
||||
|
||||
if ( req.query.suuid ) {
|
||||
req.session.suuid = req.query.suuid
|
||||
}
|
||||
|
||||
next( )
|
||||
}
|
||||
|
||||
/*
|
||||
Finalizes authentication for the main frontend application.
|
||||
*/
|
||||
let finalizeAuth = async ( req, res, next ) => {
|
||||
|
||||
if ( req.session.appId ) {
|
||||
|
||||
try {
|
||||
|
||||
let app = await getApp( { id: req.session.appId } )
|
||||
let ac = await createAuthorizationCode( { appId: app.id, userId: req.user.id, challenge: req.session.challenge } )
|
||||
|
||||
if ( req.session ) req.session.destroy( )
|
||||
return res.redirect( `/auth/finalize?appId=${app.id}&access_code=${ac}` )
|
||||
|
||||
|
||||
} catch ( err ) {
|
||||
|
||||
sentry( { err } )
|
||||
if ( req.session ) req.session.destroy( )
|
||||
return res.status( 401 ).send( 'Invalid request.' )
|
||||
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
if ( process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test' ) {
|
||||
|
||||
let token = await createPersonalAccessToken( req.user.id, 'test token', [ 'streams:write', 'streams:read', 'profile:read', 'profile:email', 'users:read', 'users:email' ] )
|
||||
if ( req.session ) req.session.destroy( )
|
||||
return res.status( 200 ).send( { userId: req.user.id, apiToken: token } )
|
||||
|
||||
|
||||
}
|
||||
try {
|
||||
let app = await getApp( { id: 'spklwebapp' } )
|
||||
let ac = await createAuthorizationCode( { appId: 'spklwebapp', userId: req.user.id, challenge: req.session.challenge } )
|
||||
|
||||
if ( req.session ) req.session.destroy( )
|
||||
return res.status( 200 ).end( )
|
||||
|
||||
return res.redirect( `${app.redirectUrl}?access_code=${ac}` )
|
||||
} catch ( err ) {
|
||||
sentry( { err } )
|
||||
if ( req.session ) req.session.destroy( )
|
||||
return res.status( 401 ).send( 'Invalid request.' )
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: add cors
|
||||
app.post( '/auth/token', async ( req, res, next ) => {
|
||||
/*
|
||||
Strategies initialisation & listing
|
||||
*/
|
||||
|
||||
let strategyCount = 0
|
||||
|
||||
if ( process.env.STRATEGY_GOOGLE === 'true' ) {
|
||||
let googStrategy = require( './strategies/google' )( app, session, sessionStorage, finalizeAuth )
|
||||
authStrategies.push( googStrategy )
|
||||
strategyCount++
|
||||
}
|
||||
|
||||
if ( process.env.STRATEGY_GITHUB === 'true' ) {
|
||||
let githubStrategy = require( './strategies/github' )( app, session, sessionStorage, finalizeAuth )
|
||||
authStrategies.push( githubStrategy )
|
||||
strategyCount++
|
||||
}
|
||||
|
||||
// Note: always leave the local strategy init for last so as to be able to
|
||||
// force enable it in case no others are present.
|
||||
if ( process.env.STRATEGY_LOCAL === 'true' || strategyCount === 0 ) {
|
||||
let localStrategy = require( './strategies/local' )( app, session, sessionStorage, finalizeAuth )
|
||||
authStrategies.push( localStrategy )
|
||||
}
|
||||
|
||||
/*
|
||||
Auth routes
|
||||
*/
|
||||
|
||||
/*
|
||||
Generates an access code for an app.
|
||||
*/
|
||||
app.get( '/auth/accesscode', async( req, res, next ) => {
|
||||
try {
|
||||
let appId = req.query.appId
|
||||
let app = await getApp( { id: appId } )
|
||||
if ( !app ) throw new Error( 'App does not exist.' )
|
||||
|
||||
let challenge = req.query.challenge
|
||||
let userToken = req.query.token
|
||||
|
||||
// 1. Validate token
|
||||
let { valid, scopes, userId, role } = await validateToken( userToken )
|
||||
if ( !valid ) throw new Error( 'Invalid token' )
|
||||
|
||||
// 2. Validate token scopes
|
||||
await validateScopes( scopes, 'tokens:write' )
|
||||
|
||||
let ac = await createAuthorizationCode( { appId, userId, challenge } )
|
||||
return res.redirect( `${app.redirectUrl}?access_code=${ac}` )
|
||||
|
||||
} catch ( err ) {
|
||||
sentry( { err } )
|
||||
debug( 'speckle:errors' )( err )
|
||||
return res.status( 400 ).send( err.message )
|
||||
}
|
||||
} )
|
||||
|
||||
/*
|
||||
Generates a new api token: (1) either via a valid refresh token or (2) via a valid access token
|
||||
*/
|
||||
app.post( '/auth/token', async ( req, res, next ) => {
|
||||
try {
|
||||
// Token refresh
|
||||
if ( req.body.refreshToken ) {
|
||||
if ( !req.body.appId || !req.body.appSecret )
|
||||
throw new Error( 'Invalid request - refresh token' )
|
||||
@@ -94,46 +130,37 @@ exports.init = ( app, options ) => {
|
||||
return res.send( authResponse )
|
||||
}
|
||||
|
||||
// Access-code - token exchange
|
||||
if ( !req.body.appId || !req.body.appSecret || !req.body.accessCode || !req.body.challenge )
|
||||
throw new Error( 'Invalid request' + JSON.stringify( req.body ) )
|
||||
|
||||
let authResponse = await createAppTokenFromAccessCode( { appId: req.body.appId, appSecret: req.body.appSecret, accessCode: req.body.accessCode, challenge: req.body.challenge } )
|
||||
return res.send( authResponse )
|
||||
|
||||
} catch ( err ) {
|
||||
|
||||
sentry( { err } )
|
||||
return res.status( 401 ).send( { err: err.message } )
|
||||
|
||||
}
|
||||
|
||||
} )
|
||||
|
||||
// TODO: add logout route
|
||||
/*
|
||||
Ensures a user is logged out by invalidating their token and refresh token.
|
||||
*/
|
||||
app.post( '/auth/logout', async ( req, res, next ) => {
|
||||
try {
|
||||
let token = req.body.token
|
||||
let refreshToken = req.body.refreshToken
|
||||
|
||||
if ( !token ) throw new Error( 'Invalid request' )
|
||||
await revokeTokenById( token )
|
||||
|
||||
// Strategies initialisation & listing
|
||||
// NOTE: if no strategies are defined, the local one will be enabled.
|
||||
if ( refreshToken )
|
||||
revokeRefreshToken( { tokenId:refreshToken } )
|
||||
|
||||
let strategyCount = 0
|
||||
|
||||
if ( process.env.STRATEGY_GITHUB === 'true' ) {
|
||||
let githubStrategy = require( './strategies/github' )( app, session, sessionAppId, finalizeAuth )
|
||||
authStrategies.push( githubStrategy )
|
||||
strategyCount++
|
||||
}
|
||||
|
||||
if ( process.env.STRATEGY_GOOGLE === 'true' ) {
|
||||
let googStrategy = require( './strategies/google' )( app, session, sessionAppId, finalizeAuth )
|
||||
authStrategies.push( googStrategy )
|
||||
strategyCount++
|
||||
}
|
||||
|
||||
// Note: always leave the local strategy init for last so as to be able to
|
||||
// force enable it in case no others are present.
|
||||
if ( process.env.STRATEGY_LOCAL === 'true' || strategyCount === 0 ) {
|
||||
let localStrategy = require( './strategies/local' )( app, session, sessionAppId, finalizeAuth )
|
||||
authStrategies.push( localStrategy )
|
||||
}
|
||||
return res.status( 200 ).send( { message: 'You have logged out.' } )
|
||||
|
||||
} catch ( err ){
|
||||
sentry( { err } )
|
||||
return res.status( 400 ).send( { err: err.message } )
|
||||
}
|
||||
} )
|
||||
}
|
||||
|
||||
@@ -20,10 +20,9 @@ exports.up = async knex => {
|
||||
|
||||
const desktopConnectorScopes = [
|
||||
{ appId: 'sdm', scopeName: 'streams:read' },
|
||||
{ appId: 'sdm', scopeName: 'streams:write' },
|
||||
{ appId: 'sdm', scopeName: 'profile:read' },
|
||||
{ appId: 'sdm', scopeName: 'profile:email' },
|
||||
{ appId: 'sdm', scopeName: 'users:read' },
|
||||
{ appId: 'sdm', scopeName: 'users:read' }
|
||||
]
|
||||
await knex( 'server_apps_scopes' ).insert( desktopConnectorScopes )
|
||||
|
||||
@@ -39,7 +38,7 @@ exports.up = async knex => {
|
||||
`,
|
||||
trustByDefault: true,
|
||||
public: true,
|
||||
redirectUrl: 'self'
|
||||
redirectUrl: process.env.CANONICAL_URL
|
||||
} )
|
||||
|
||||
const scopes = await knex( 'scopes' ).select( '*' )
|
||||
@@ -56,7 +55,7 @@ exports.up = async knex => {
|
||||
description: 'GraphQL Playground with authentication.',
|
||||
trustByDefault: true,
|
||||
public: true,
|
||||
redirectUrl: '/explorer',
|
||||
redirectUrl: `${process.env.CANONICAL_URL}/explorer`
|
||||
} )
|
||||
|
||||
const explorerScopes = scopes.filter( s => s.name !== 'server:setup' ).map( s => ( { appId: 'explorer', scopeName: s.name } ) )
|
||||
@@ -70,10 +69,10 @@ exports.up = async knex => {
|
||||
secret: '12345',
|
||||
name: 'Mock Application',
|
||||
description: 'Lorem ipsum dolor sic amet.',
|
||||
redirectUrl: 'http://localhost:1337', // ie, will just redirect to window.location
|
||||
redirectUrl: 'http://localhost:1337'
|
||||
} )
|
||||
|
||||
const mockAppScopes = [ { appId: 'mock', scopeName: 'streams:read' }, { appId: 'mock', scopeName: 'users:read' }, { appId: 'mock', scopeName: 'profile:email' } ]
|
||||
const mockAppScopes = [ { appId: 'mock', scopeName: 'streams:read' }, { appId: 'mock', scopeName: 'streams:write' }, { appId: 'mock', scopeName: 'users:read' }, { appId: 'mock', scopeName: 'profile:email' } ]
|
||||
await knex( 'server_apps_scopes' ).insert( mockAppScopes )
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ module.exports = {
|
||||
|
||||
let appScopeNames = ( await ServerAppsScopes( ).select( 'scopeName' ).where( { appId: id } ) ).map( s => s.scopeName )
|
||||
app.scopes = allScopes.filter( scope => appScopeNames.indexOf( scope.name ) !== -1 )
|
||||
app.author = await Users( ).select( 'id', 'name' ).where( { id: app.authorId } ).first( )
|
||||
app.author = await Users( ).select( 'id', 'name', 'avatar' ).where( { id: app.authorId } ).first( )
|
||||
return app
|
||||
|
||||
},
|
||||
@@ -134,6 +134,15 @@ module.exports = {
|
||||
|
||||
},
|
||||
|
||||
async revokeRefreshToken( { tokenId } ) {
|
||||
tokenId = tokenId.slice( 0, 10 )
|
||||
let delCount = await RefreshTokens( ).where( { id: tokenId } ).del( )
|
||||
|
||||
if ( delCount === 0 )
|
||||
throw new Error( 'Did not revoke token' )
|
||||
return true
|
||||
},
|
||||
|
||||
async revokeExistingAppCredentials( { appId } ) {
|
||||
|
||||
let resAccessCodeDelete = await AuthorizationCodes( ).where( { appId: appId } ).del( )
|
||||
|
||||
@@ -8,13 +8,13 @@ const appRoot = require( 'app-root-path' )
|
||||
const { findOrCreateUser } = require( `${appRoot}/modules/core/services/users` )
|
||||
const { getApp, createAuthorizationCode, createAppTokenFromAccessCode } = require( '../services/apps' )
|
||||
|
||||
module.exports = ( app, session, sessionAppId, finalizeAuth ) => {
|
||||
module.exports = ( app, session, sessionStorage, finalizeAuth ) => {
|
||||
const strategy = {
|
||||
id: 'github',
|
||||
name: 'Github',
|
||||
icon: 'TODO',
|
||||
color: 'grey darken-2',
|
||||
url: `/auth/gh`,
|
||||
icon: 'mdi-github',
|
||||
color: 'grey darken-3',
|
||||
url: '/auth/gh',
|
||||
callbackUrl: ( new URL( '/auth/gh/callback', process.env.CANONICAL_URL ) ).toString( )
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ module.exports = ( app, session, sessionAppId, finalizeAuth ) => {
|
||||
|
||||
passport.use( myStrategy )
|
||||
|
||||
app.get( strategy.url, session, sessionAppId, passport.authenticate( 'github', { failureRedirect: '/auth/error' } ) )
|
||||
app.get( strategy.url, session, sessionStorage, passport.authenticate( 'github', { failureRedirect: '/auth/error' } ) )
|
||||
app.get( '/auth/gh/callback', session, passport.authenticate( 'github', { failureRedirect: '/auth/error' } ), finalizeAuth )
|
||||
|
||||
return strategy
|
||||
|
||||
@@ -7,12 +7,12 @@ const appRoot = require( 'app-root-path' )
|
||||
const { findOrCreateUser } = require( `${appRoot}/modules/core/services/users` )
|
||||
const { getApp, createAuthorizationCode, createAppTokenFromAccessCode } = require( '../services/apps' )
|
||||
|
||||
module.exports = ( app, session, sessionAppId, finalizeAuth ) => {
|
||||
module.exports = ( app, session, sessionStorage, finalizeAuth ) => {
|
||||
const strategy = {
|
||||
id: 'google',
|
||||
name: 'Google',
|
||||
icon: 'TODO',
|
||||
color: 'white red--text',
|
||||
icon: 'mdi-google',
|
||||
color: 'red darken-3',
|
||||
url: '/auth/goog',
|
||||
callbackUrl: '/auth/goog/callback'
|
||||
}
|
||||
@@ -40,7 +40,7 @@ module.exports = ( app, session, sessionAppId, finalizeAuth ) => {
|
||||
|
||||
passport.use( myStrategy )
|
||||
|
||||
app.get( strategy.url, session, sessionAppId, passport.authenticate( 'google' ) )
|
||||
app.get( strategy.url, session, sessionStorage, passport.authenticate( 'google' ) )
|
||||
app.get( '/auth/goog/callback', session, passport.authenticate( 'google', { failureRedirect: '/auth/error' } ), finalizeAuth )
|
||||
|
||||
return strategy
|
||||
|
||||
@@ -12,25 +12,28 @@ module.exports = ( app, session, sessionAppId, finalizeAuth ) => {
|
||||
name: 'Local',
|
||||
icon: 'TODO',
|
||||
color: 'accent',
|
||||
url: `/auth/local`
|
||||
url: '/auth/local'
|
||||
}
|
||||
|
||||
app.post( '/auth/local/login', session, sessionAppId, async ( req, res, next ) => {
|
||||
let valid = await validatePasssword( { email: req.body.email, password: req.body.password } )
|
||||
try {
|
||||
let valid = await validatePasssword( { email: req.body.email, password: req.body.password } )
|
||||
|
||||
if ( !valid ) {
|
||||
if ( !valid ) throw new Error( 'Invalid credentials' )
|
||||
|
||||
let user = await getUserByEmail( { email: req.body.email } )
|
||||
if ( !user ) throw new Error( 'Invalid credentials' )
|
||||
|
||||
if ( req.body.suuid && user.suuid !== req.body.suuid ) {
|
||||
await updateUser( user.id, { suuid: req.body.suuid } )
|
||||
}
|
||||
|
||||
req.user = { id: user.id }
|
||||
|
||||
next( )
|
||||
} catch ( err ){
|
||||
return res.status( 401 ).send( { err: true, message: 'Invalid credentials' } )
|
||||
}
|
||||
|
||||
let user = await getUserByEmail( { email: req.body.email } )
|
||||
|
||||
if ( req.body.suuid && user.suuid !== req.body.suuid ) {
|
||||
await updateUser( user.id, { suuid: req.body.suuid } )
|
||||
}
|
||||
|
||||
req.user = { id: user.id }
|
||||
|
||||
next( )
|
||||
}, finalizeAuth )
|
||||
|
||||
app.post( '/auth/local/register', session, sessionAppId, async ( req, res, next ) => {
|
||||
|
||||
@@ -5,26 +5,26 @@ let debug = require( 'debug' )( 'speckle:modules' )
|
||||
exports.up = async knex => {
|
||||
debug( 'Setting up core module scopes.' )
|
||||
|
||||
let coreModuleScopes = [
|
||||
let coreModuleScopes = [
|
||||
{
|
||||
name: 'streams:read',
|
||||
description: 'Read your streams & and any associated information (branches, tags, comments, objects, etc.)'
|
||||
description: 'Read your streams, and any associated information (branches, commits, objects).'
|
||||
},
|
||||
{
|
||||
name: 'streams:write',
|
||||
description: 'Create streams on your behalf and read your streams & any associated information (any associated information (branches, tags, comments, objects, etc.)'
|
||||
description: 'Create streams on your behalf, and any associated data (branches, commits, objects).'
|
||||
},
|
||||
{
|
||||
name: 'profile:read',
|
||||
description: `Read your profile information.`
|
||||
description: 'Read your profile information (name, bio, company).'
|
||||
},
|
||||
{
|
||||
name: 'profile:email',
|
||||
description: `Access your email.`
|
||||
description: 'Grants access to the email address you registered with.'
|
||||
},
|
||||
{
|
||||
name: 'users:read',
|
||||
description: `Read other users' profile on your behalf.`
|
||||
description: 'Read other users\' profile on your behalf.'
|
||||
},
|
||||
{
|
||||
name: 'users:email',
|
||||
@@ -36,12 +36,12 @@ exports.up = async knex => {
|
||||
},
|
||||
{
|
||||
name: 'tokens:read',
|
||||
description: `Access your api tokens.`
|
||||
description: 'Access your api tokens.'
|
||||
},
|
||||
{
|
||||
name: 'tokens:write',
|
||||
description: `Create and delete api tokens on your behalf.`
|
||||
}]
|
||||
description: 'Create and delete api tokens on your behalf.'
|
||||
} ]
|
||||
|
||||
await knex( 'scopes' ).insert( coreModuleScopes )
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ const { createObjects, createObjectsBatched } = require( '../services/objects' )
|
||||
module.exports = ( app ) => {
|
||||
app.post( '/objects/:streamId', contextMiddleware, async ( req, res ) => {
|
||||
|
||||
debug( 'speckle:upload-endpoint' )( `booom upload endpoint` )
|
||||
debug( 'speckle:upload-endpoint' )( 'booom upload endpoint' )
|
||||
|
||||
if ( !req.context || !req.context.auth ) {
|
||||
return res.status( 401 ).end( )
|
||||
@@ -29,7 +29,7 @@ module.exports = ( app ) => {
|
||||
return res.status( 401 ).end( )
|
||||
}
|
||||
|
||||
debug( 'speckle:upload-endpoint' )( `Upload started` )
|
||||
debug( 'speckle:upload-endpoint' )( 'Upload started' )
|
||||
|
||||
let busboy = new Busboy( { headers: req.headers } )
|
||||
let totalProcessed = 0
|
||||
@@ -57,7 +57,7 @@ module.exports = ( app ) => {
|
||||
objs = JSON.parse( gunzipedBuffer )
|
||||
} catch ( e ) {
|
||||
requestDropped = true
|
||||
return res.status( 400 ).send( `Failed to parse data.` )
|
||||
return res.status( 400 ).send( 'Failed to parse data.' )
|
||||
}
|
||||
|
||||
last = objs[ objs.length - 1 ]
|
||||
@@ -82,7 +82,7 @@ module.exports = ( app ) => {
|
||||
objs = JSON.parse( buffer )
|
||||
} catch ( e ) {
|
||||
requestDropped = true
|
||||
return res.status( 400 ).send( `Failed to parse data.` )
|
||||
return res.status( 400 ).send( 'Failed to parse data.' )
|
||||
}
|
||||
last = objs[ objs.length - 1 ]
|
||||
totalProcessed += objs.length
|
||||
@@ -94,7 +94,7 @@ module.exports = ( app ) => {
|
||||
} )
|
||||
} else {
|
||||
requestDropped = true
|
||||
return res.status( 400 ).send( `Invalid ContentType header. This route only accepts "application/gzip", "text/plain" or "application/json".` )
|
||||
return res.status( 400 ).send( 'Invalid ContentType header. This route only accepts "application/gzip", "text/plain" or "application/json".' )
|
||||
}
|
||||
} )
|
||||
|
||||
@@ -105,7 +105,7 @@ module.exports = ( app ) => {
|
||||
|
||||
await Promise.all( promises )
|
||||
|
||||
debug( 'speckle:upload-endpoint' )( `Upload ended` )
|
||||
debug( 'speckle:upload-endpoint' )( 'Upload ended' )
|
||||
res.status( 201 ).end( )
|
||||
} )
|
||||
|
||||
|
||||
@@ -92,7 +92,6 @@ module.exports = {
|
||||
|
||||
async revokeToken( tokenId, userId ) {
|
||||
tokenId = tokenId.slice( 0, 10 )
|
||||
let token = await ApiTokens( ).where( { id: tokenId } ).select( "*" )
|
||||
let delCount = await ApiTokens( ).where( { id: tokenId, owner: userId } ).del( )
|
||||
|
||||
if ( delCount === 0 )
|
||||
|
||||
@@ -29,8 +29,8 @@ async function contextApiTokenHelper( { req, res, connection } ) {
|
||||
token = req.headers.authorization
|
||||
}
|
||||
|
||||
if ( token && token.includes( "Bearer " ) ) {
|
||||
token = token.split( " " )[ 1 ]
|
||||
if ( token && token.includes( 'Bearer ' ) ) {
|
||||
token = token.split( ' ' )[ 1 ]
|
||||
}
|
||||
|
||||
if ( token === null )
|
||||
|
||||
Reference in New Issue
Block a user