Artsy Passport
Wires up the common auth handlers, and related security concerns, for Artsy's Ezel-based apps using passport. Used internally at Artsy to DRY up authentication code.
Breaking changes in 2.0
- The app is now shipped as a single JS file.
Setup
artsy-xapp.
Make sure you first mount session, body parser, and startappuse expressbodyParserappuse expresscookieParser'foobar'appuse expresscookieSessionartsyXappinit -> applisten
Then mount Artsy Passport passing a big configuration hash.
Values indicate defaults.
appuse artsyPassport CurrentUser: # The CurrentUser Backbone model # Pass in env vars # ---------------- FACEBOOK_ID: # Facebook app ID FACEBOOK_SECRET: # Facebook app secret TWITTER_KEY: # Twitter consumer key TWITTER_SECRET: # Twitter consumer secret TWITTER_KEY: # Twitter consumer key TWITTER_SECRET: # Twitter consumer secret LINKEDIN_KEY: # Linkedin app key LINKEDIN_SECRET: # Linkedin app secret ARTSY_ID: # Artsy client id ARTSY_SECRET: # Artsy client secret ARTSY_URL: # SSL Artsy url e.g. https://artsy.net APP_URL: # Url pointing back to your app e.g. http://flare.artsy.net SEGMENT_WRITE_KEY: # Segment write key to track signup # Defaults you probably don't need to touch # ----------------------------------------- # Social auth linkedinPath: '/users/auth/linkedin' linkedinCallbackPath: '/users/auth/linkedin/callback' facebookPath: '/users/auth/facebook' facebookCallbackPath: '/users/auth/facebook/callback' twitterPath: '/users/auth/twitter' twitterCallbackPath: '/users/auth/twitter/callback' twitterLastStepPath: '/users/auth/twitter/email' : hash = cryptocreateHash'sha1'updatetokendigest'hex' "@artsy.tmp" # Landing pages loginPagePath: '/log_in' signupPagePath: '/sign_up' settingsPagePath: '/user/edit' afterSignupPagePath: '/personalize' # Misc logoutPath: '/users/sign_out' userKeys: 'id''type''name''email''phone''lab_features' 'default_profile_id''has_partner_access''collector_level'
The keys are cased so it's convenient to pass in a configuration hash. A minimal setup could look like this:
appuse artsyPassport _extend config CurrentUser: CurrentUser
Note: CurrentUser must be a Backbone model with typical get
and toJSON
methods.
Create a login form pointing to your paths.
h1 Loginpre!= errora( href=ap.facebookPath ) Login via Facebooka( href=ap.twitterPath ) Login via Twitterform( action=ap.loginPagePath, method='POST' ) h3 Login via Email input( name='name' ) input( name='email' ) input( name='password' ) input( type="hidden" name="_csrf" value=csrfToken ) button( type='submit' ) Login
And maybe a signup form...
h1 Signuppre!= errora( href=ap.facebookPath ) Signup via Facebooka( href=ap.twitterPath ) Signup via Twitterform( action=ap.signupPagePath, method='POST' ) h3 Signup via Email input( name='name' ) input( name='email' ) input( name='password' ) input( type="hidden" name="_csrf" value=csrfToken ) button( type='submit' ) Signup
And maybe a settings page for linking accounts...
h2 Linked Accountspre!= error- providers = user.get('authentications').map(function(a) { return a.provider })if providers.indexOf('facebook') > -1 | Connected Facebookelse a( href=ap.facebookPath ) Connect Facebookbrif providers.indexOf('twitter') > -1 | Connected Twitterelse a( href=ap.twitterPath ) Connect Twitterbrif providers.indexOf('linkedin') > -1 | Connected LinkedInelse a( href=ap.linkedinPath ) Connect LinkedIn
Finally there's this weird "one last step" UI for twitter to store emails after signup.
h1 Just one more steppre!= errorform( method='post', action=ap.twitterLastStepPath ) input( type="hidden" name="_csrf" value=csrfToken ) input.bordered-input( name='email' ) button( type='submit' ) Join Artsy
Render the pages
loginPagePathsignupPagePathsettingsPagePath afterSignupPagePathtwitterLastStepPath = artsyPassportoptions appget loginPagePath resrender 'login'appget signupPagePath resrender 'signup'appget settingsPagePath resrender 'settings'appget afterSignupPagePath resrender 'personalize'appget twitterLastStepPath resrender 'twitter_last_step'
Access a logged in Artsy user in a variety of ways...
In your server-side templates
h1 Hello #{user.get('name')}
In your client-side code
CurrentUser = require '../models/current_user.coffee'sd = require'sharify'data user = sdCURRENT_USER
In your routers
appget '/' ressend 'Hello ' + requserget'name'
These forms of user will be null if they're not logged in.
Sanitize Redirect
If you implement a fancier auth flow that involves client-side redirecting back, you may find this helper useful in avoiding "open redirect" attacks.
sanitizeRedirect = require 'artsy-passport/sanitize-redirect' location.href = sanitizeRedirect "http://artsy.net%0D%0Aattacker.com/"# Notices the url isn't pointing at artsy.net, so just redirects to /
Contributing
Add a local.artsy.net
entry into your /etc/hosts
127.0.0.1 localhost
#...
127.0.0.1 local.artsy.net
Install node modules npm install
then write a ./config.coffee that looks something like this:
module.exports = FACEBOOK_ID: '' FACEBOOK_SECRET: '' TWITTER_KEY: '' TWITTER_SECRET: '' LINKEDIN_KEY: '' LINKEDIN_SECRET: '' ARTSY_ID: '' ARTSY_SECRET: '' ARTSY_URL: 'https://api.artsy.net' APP_URL: 'http://local.artsy.net:4000' # An Artsy user that's linked to Facebook and Twitter ARTSY_EMAIL: 'craig@artsy.net' ARTSY_PASSWORD: '' TWITTER_EMAIL: 'craigspaeth@gmail.com' TWITTER_PASSWORD: '' FACEBOOK_EMAIL: 'craigspaeth@gmail.com' FACEBOOK_PASSWORD: ''
Then you can check the example by running npm run example
and opening localhost:4000.
The tests are a combination of integration and middleware unit tests. To run the whole suite use npm test
.