diff --git a/.circleci/config.yml b/.circleci/config.yml index f9918ac0e..960212899 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -21,6 +21,9 @@ jobs: DATABASE_URL: 'postgres://speckle:speckle@localhost:5432/speckle2_test' PGDATABASE: speckle2_test PGUSER: speckle + SESSION_SECRET: 'keyboard cat' + STRATEGY_LOCAL: true + CANONICAL_URL: 'http://localhost:3000' steps: - checkout diff --git a/modules/apps/graph/resolvers/apps.js b/modules/apps/graph/resolvers/apps.js index f0e463bd7..44c8b2365 100644 --- a/modules/apps/graph/resolvers/apps.js +++ b/modules/apps/graph/resolvers/apps.js @@ -19,26 +19,5 @@ module.exports = { } }, Mutation: { - async appAuthorize( parent, args, context, info ) { - await validateServerRole( context, 'server:user' ) - await validateScopes( context.scopes, 'apps:authorize' ) // TODO - - // Implicit grant flow: returns the token directly - // let token = await createAppToken( { userId: context.userId, appId: args.appId } ) - // return token - - // TODO: Implement authorization code grant - let accessCode = await createAuthorizationCode( { userId: contex.userId, appId: args.appId, challenge: args.challenge } ) - return accessCode - }, - async appGetToken( parent, args, context, info ) { - - let result = await exchangeAuthorizationCodeForToken( { appId: args.appId, appSecret: args.appSecret, accessCode: args.accessCode, challenge: args.challenge } ) - // args.appId, args.appSecret, args.accessCode - - }, - async appRefreshToken( parent, args, context, info ) { - // TODO - } } } \ No newline at end of file diff --git a/modules/apps/graph/schemas/auth.graphql b/modules/apps/graph/schemas/auth.graphql index 4bd678689..b82dace65 100644 --- a/modules/apps/graph/schemas/auth.graphql +++ b/modules/apps/graph/schemas/auth.graphql @@ -31,26 +31,3 @@ type AuthStrategy { url: String!, color: String } -extend type Mutation { - """ - Authorizes an app on behalf of a user. Returns an access code that can be exchanged - by the application for an api token. - """ - appAuthorize( appId: String!, challenge: String! ): String! - """ - Exchanges an access code for an api token. - """ - appGetToken( appId: String!, appSecret: String!, accesCode: String!, challenge: String! ): AppTokenResponse! - """ - Refreshes an expired token. - """ - appRefreshToken( appId: String, appSecret:String!, refreshToken: String! ): AppTokenResponse! -} - -type AppTokenResponse { - """ - The actual bearer token. - """ - token: String! - refreshToken: String! -} diff --git a/modules/apps/index.js b/modules/apps/index.js index 8413a1736..0195abecb 100644 --- a/modules/apps/index.js +++ b/modules/apps/index.js @@ -35,9 +35,17 @@ exports.init = ( app, options ) => { } let finalizeAuth = async ( req, res, next ) => { - let app = await getApp( { id: req.session.appId } ) - let ac = await createAuthorizationCode( { appId: app.id, userId: req.user.id, challenge: req.session.challenge } ) - return res.redirect( `/auth/finalize?appId=${req.session.appId}&access_code=${ac}` ) + 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 } ) + return res.redirect( `/auth/finalize?appId=${req.session.appId}&access_code=${ac}` ) + } catch ( err ) { + return res.status( 400 ).send( err.message ) + } + } else { + return res.status( 200 ).end( ) + } } // TODO: add cors @@ -66,13 +74,17 @@ exports.init = ( app, options ) => { // Strategies initialisation & listing - let githubStrategy = require( './strategies/github' )( app, session, sessionAppId, finalizeAuth ) - authStrategies.push( githubStrategy ) + if ( process.env.STRATEGY_GITHUB === 'true' ) { + let githubStrategy = require( './strategies/github' )( app, session, sessionAppId, finalizeAuth ) + authStrategies.push( githubStrategy ) + } - let googStrategy = require( './strategies/google' )( app, session, sessionAppId, finalizeAuth ) - authStrategies.push( googStrategy ) + if ( process.env.STRATEGY_GOOGLE === 'true' ) { + let googStrategy = require( './strategies/google' )( app, session, sessionAppId, finalizeAuth ) + authStrategies.push( googStrategy ) + } - if ( process.env.STRATEGY_LOCAL ) { + if ( process.env.STRATEGY_LOCAL === 'true' ) { let localStrategy = require( './strategies/local' )( app, session, sessionAppId, finalizeAuth ) authStrategies.push( localStrategy ) } diff --git a/modules/apps/strategies/github.js b/modules/apps/strategies/github.js index 5c6f2a3b8..e23f6e1bf 100644 --- a/modules/apps/strategies/github.js +++ b/modules/apps/strategies/github.js @@ -1,3 +1,4 @@ +/* istanbul ignore file */ 'use strict' const passport = require( 'passport' ) diff --git a/modules/apps/strategies/google.js b/modules/apps/strategies/google.js index d79d3a665..d1bce4472 100644 --- a/modules/apps/strategies/google.js +++ b/modules/apps/strategies/google.js @@ -1,3 +1,4 @@ +/* istanbul ignore file */ 'use strict' const passport = require( 'passport' ) const GoogleStrategy = require( 'passport-google-oauth20' ).Strategy diff --git a/modules/apps/strategies/local.js b/modules/apps/strategies/local.js index 0f003e338..172f86dc2 100644 --- a/modules/apps/strategies/local.js +++ b/modules/apps/strategies/local.js @@ -28,6 +28,9 @@ module.exports = ( app, session, sessionAppId, finalizeAuth ) => { app.post( '/auth/local/register', session, sessionAppId, async ( req, res, next ) => { try { + if ( !req.body.password ) + throw new Error( 'Password missing' ) + let userId = await createUser( req.body ) req.user = { id: userId } return next( ) diff --git a/modules/apps/tests/apps.spec.js b/modules/apps/tests/apps.spec.js index f0acb04fb..4c07a2784 100644 --- a/modules/apps/tests/apps.spec.js +++ b/modules/apps/tests/apps.spec.js @@ -1,5 +1,6 @@ const chai = require( 'chai' ) const chaiHttp = require( 'chai-http' ) +const request = require( 'supertest' ) const assert = require( 'assert' ) const appRoot = require( 'app-root-path' ) @@ -17,89 +18,136 @@ const { getApp, registerApp, createAuthorizationCode, createAppTokenFromAccessCo describe( 'Apps', ( ) => { - let actor = { - username: 'DimitrieStefanescu', - name: 'Dimitrie Stefanescu', - email: 'didimitrie@gmail.com', - password: 'wtfwtfwtf' - } + describe( 'Services', ( ) => { + let actor = { + username: 'DimitrieStefanescu', + name: 'Dimitrie Stefanescu', + email: 'didimitrie@gmail.com', + password: 'wtfwtfwtf' + } - before( async ( ) => { - await knex.migrate.rollback( ) - await knex.migrate.latest( ) - actor.id = await createUser( actor ) + before( async ( ) => { + await knex.migrate.rollback( ) + await knex.migrate.latest( ) + actor.id = await createUser( actor ) + } ) + + after( async ( ) => { + + } ) + + it( 'Should get the frontend main app', async ( ) => { + let app = await getApp( { id: 'spklwebapp' } ) + expect( app ).to.be.an( 'object' ) + expect( app.redirectUrl ).to.be.a( 'string' ) + expect( app.scopes ).to.be.a( 'array' ) + expect( app.firstparty ).to.equal( true ) + } ) + + it( 'Should get the mock app', async ( ) => { + let app = await getApp( { id: 'mock' } ) + expect( app ).to.be.an( 'object' ) + expect( app.redirectUrl ).to.be.a( 'string' ) + expect( app.scopes ).to.be.a( 'array' ) + expect( app.firstparty ).to.equal( false ) + } ) + + let myTestApp = null + + it( 'Should register an app', async ( ) => { + let res = await registerApp( { name: 'test application', firstparty: true, author: actor.id, scopes: [ 'streams:read' ], redirectUrl: 'http://localhost:1335' } ) + + expect( res ).to.have.property( 'id' ) + expect( res ).to.have.property( 'secret' ) + + expect( res.id ).to.be.a( 'string' ) + expect( res.secret ).to.be.a( 'string' ) + myTestApp = res + + let app = await getApp( { id: res.id } ) + expect( app.firstparty ).to.equal( false ) + expect( app.id ).to.equal( res.id ) + } ) + + let challenge = 'random' + let authorizationCode = null + it( 'Should get an authorization code for the app', async ( ) => { + authorizationCode = await createAuthorizationCode( { appId: myTestApp.id, userId: actor.id, challenge } ) + expect( authorizationCode ).to.be.a( 'string' ) + } ) + + let tokenCreateResponse = null + it( 'Should get an api token in exchange for the authorization code ', async ( ) => { + let response = await createAppTokenFromAccessCode( { appId: myTestApp.id, appSecret: myTestApp.secret, accessCode: authorizationCode, challenge: 'random' } ) + expect( response ).to.have.property( 'token' ) + expect( response.token ).to.be.a( 'string' ) + expect( response ).to.have.property( 'refreshToken' ) + expect( response.refreshToken ).to.be.a( 'string' ) + + tokenCreateResponse = response + + let validation = await validateToken( response.token ) + expect( validation.valid ).to.equal( true ) + expect( validation.userId ).to.equal( actor.id ) + expect( validation.scopes[ 0 ] ).to.equal( 'streams:read' ) + } ) + + it( 'Should refresh the token using the refresh token, and get a fresh refresh token and token', async ( ) => { + let res = await refreshAppToken( { refreshToken: tokenCreateResponse.refreshToken, appId: myTestApp.id, appSecret: myTestApp.secret, userId: actor.id } ) + + expect( res.token ).to.be.a( 'string' ) + expect( res.refreshToken ).to.be.a( 'string' ) + + let validation = await validateToken( res.token ) + expect( validation.valid ).to.equal( true ) + expect( validation.userId ).to.equal( actor.id ) + } ) } ) - after( async ( ) => { + describe( 'Local authN', ( ) => { + let expressApp + before( async ( ) => { + await knex.migrate.rollback( ) + await knex.migrate.latest( ) + let { app } = await init( ) + expressApp = app + } ) + + after( async ( ) => { + await knex.migrate.rollback( ) + } ) + + it( 'Should register a new user', async ( ) => { + let res = + await request( expressApp ) + .post( `/auth/local/register` ) + .send( { email: 'spam@speckle.systems', name: 'dimitrie stefanescu', username: 'dimitrie', company: 'speckle', password: 'roll saving throws' } ) + .expect( 200 ) + } ) + + it( 'Should fail to register a new user w/o password', async ( ) => { + let res = + await request( expressApp ) + .post( `/auth/local/register` ) + .send( { email: 'spam@speckle.systems', name: 'dimitrie stefanescu', username: 'dimitrie' } ) + .expect( 400 ) + } ) + + it( 'Should log in ', async ( ) => { + let res = + await request( expressApp ) + .post( `/auth/local/login` ) + .send( { email: 'spam@speckle.systems', password: 'roll saving throws' } ) + .expect( 200 ) + } ) + + it( 'Should fail nicely to log in ', async ( ) => { + let res = + await request( expressApp ) + .post( `/auth/local/login` ) + .send( { email: 'spam@speckle.systems', password: 'roll saving throw' } ) + .expect( 401 ) + } ) } ) - - it( 'Should get the frontend main app', async ( ) => { - let app = await getApp( { id: 'spklwebapp' } ) - expect( app ).to.be.an( 'object' ) - expect( app.redirectUrl ).to.be.a( 'string' ) - expect( app.scopes ).to.be.a( 'array' ) - expect( app.firstparty ).to.equal( true ) - } ) - - it( 'Should get the mock app', async ( ) => { - let app = await getApp( { id: 'mock' } ) - expect( app ).to.be.an( 'object' ) - expect( app.redirectUrl ).to.be.a( 'string' ) - expect( app.scopes ).to.be.a( 'array' ) - expect( app.firstparty ).to.equal( false ) - } ) - - let myTestApp = null - - it( 'Should register an app', async ( ) => { - let res = await registerApp( { name: 'test application', firstparty: true, author: actor.id, scopes: [ 'streams:read' ], redirectUrl: 'http://localhost:1335' } ) - - expect( res ).to.have.property( 'id' ) - expect( res ).to.have.property( 'secret' ) - - expect( res.id ).to.be.a( 'string' ) - expect( res.secret ).to.be.a( 'string' ) - myTestApp = res - - let app = await getApp( { id: res.id } ) - expect( app.firstparty ).to.equal( false ) - expect( app.id ).to.equal( res.id ) - } ) - - let challenge = 'random' - let authorizationCode = null - it( 'Should get an authorization code for the app', async ( ) => { - authorizationCode = await createAuthorizationCode( { appId: myTestApp.id, userId: actor.id, challenge } ) - expect( authorizationCode ).to.be.a( 'string' ) - } ) - - let tokenCreateResponse = null - it( 'Should get an api token in exchange for the authorization code ', async ( ) => { - let response = await createAppTokenFromAccessCode( { appId: myTestApp.id, appSecret: myTestApp.secret, accessCode: authorizationCode, challenge: 'random' } ) - expect( response ).to.have.property( 'token' ) - expect( response.token ).to.be.a( 'string' ) - expect( response ).to.have.property( 'refreshToken' ) - expect( response.refreshToken ).to.be.a( 'string' ) - - tokenCreateResponse = response - - let validation = await validateToken( response.token ) - expect( validation.valid ).to.equal( true ) - expect( validation.userId ).to.equal( actor.id ) - expect( validation.scopes[ 0 ] ).to.equal( 'streams:read' ) - } ) - - it( 'Should refresh the token using the refresh token, and get a fresh refresh token and token', async ( ) => { - let res = await refreshAppToken( { refreshToken: tokenCreateResponse.refreshToken, appId: myTestApp.id, appSecret: myTestApp.secret, userId: actor.id } ) - - expect( res.token ).to.be.a( 'string' ) - expect( res.refreshToken ).to.be.a( 'string' ) - - let validation = await validateToken( res.token ) - expect( validation.valid ).to.equal( true ) - expect( validation.userId ).to.equal( actor.id ) - } ) - - } ) \ No newline at end of file