Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace JWT authentication with database-backed sessions #9

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,3 @@
STRIPE_PUBLISHABLE_KEY=<Your Stripe publishable key>
STRIPE_SECRET_KEY=<Your Stripe secret key>
STRIPE_API_VERSION=2018-05-21

# JSON web token (JWT) secret: this keeps our app's user authentication secure
# This secret should be a random 20-character string of characters, e.g. 'oj2130sdjk120asdim2u2'
JWT_SECRET=<A random 20-character string>
7 changes: 2 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ Typographic is a complete, full-stack example of a Stripe Billing integration:
🌏|**Vue.js frontend.** Single-page [Vue](https://vuejs.org) app demonstrating how to use Elements in a component-based web framework.
☕️|**Node.js backend.** An [Express](https://expressjs.com/) server manages billing and user data between the database and Stripe's API.
📦|**Database support.** Uses [Knex.js](http://knexjs.org/) and [SQLite](https://www.sqlite.org/index.html) (by default) to demonstrate a data modeling pattern for the [Billing](https://stripe.com/docs/billing/quickstart) API.
🔑|**User authentication.** JSON web tokens ([JWT](https://jwt.io/)) and an Express authentication scheme are included for user login and registration.
🔑|**User authentication.** An Express authentication scheme is included for user login and registration.

## Stripe Billing Integration

Expand Down Expand Up @@ -66,10 +66,7 @@ Install dependencies using npm (or yarn):
npm install
```

Copy the example .env file. You'll need to fill out two details:

- Your [Stripe API keys](https://dashboard.stripe.com/account/apikeys)
- A random 20-character string to keep user authentication secure (using [JSON Web Tokens](https://jwt.io))
Copy the example .env file, and fill in your [Stripe API keys](https://dashboard.stripe.com/account/apikeys).

```
cp .env.example .env
Expand Down
43 changes: 18 additions & 25 deletions client/src/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,55 +2,48 @@
* auth.js
* Stripe Billing demo. Created by Michael Glukhovsky (@mglukhovsky).
*
* Frontend authentication: this manages our user authentication and JSON web
* token (JWT) storage in the browser.
* Frontend authentication: this manages our user authentication and session
* token storage in the browser.
*/

import axios from 'axios';
import Vue from 'vue';
import jwtDecode from 'jwt-decode';

export default {
// Try to retrieve our JSON web token (JWT) from the session storage
// Try to retrieve our session token
token: sessionStorage.getItem('token'),
// Check our token to make sure it's valid
hasValidToken() {
if (this.token) {
// Decode the token and check its data. The `exp` property is a timestamp
// when this token will expire.
const decoded = jwtDecode(this.token);
if (decoded.exp > Date.now()) {
// The token expired, so log out the user.
this.logout();
return false;
}
return true;
}
return false;
},
// Store the JWT and user credentials
// Store the session token
setToken(token) {
// Add the token to our session
this.token = token;
sessionStorage.setItem('token', token);
// Include a header with outgoing requests
this.setHeader();
},
// Log out the user
logout() {
// Remove the HTTP header that include the JWT token
// Remove the session token locally
clearToken() {
// Remove the HTTP header that include the session token
delete axios.defaults.headers.common['Authorization'];
// Delete the token from our session
sessionStorage.removeItem('token');
this.token = null;
},
// Log out the user
async logout() {
try {
await axios.post('/auth/logout');
} catch (e) {
console.warn(e);
}
this.clearToken();
},
// Check if the user is logged in
loggedIn() {
return !!this.token;
},
// Instruct Vue to include a header with the JWT in every request
// Instruct Vue to include a header with the session token in every request
setHeader() {
if (this.hasValidToken()) {
if (this.loggedIn()) {
axios.defaults.headers.common['Authorization'] = `Bearer ${this.token}`;
}
},
Expand Down
1 change: 1 addition & 0 deletions client/src/components/Account.vue
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@
<div class="upgrade-plan" v-if="extraRequests > 0">
<p>Want to increase your included requests?</p>
<button class="upgrade" @click="$router.push('/pricing')">Upgrade your plan</button>
</div>
</div>
</section>
</template>
Expand Down
2 changes: 1 addition & 1 deletion client/src/components/AppNav.vue
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export default {
this.showingDropdown = !this.showingDropdown;
e.stopPropagation();
},
logout: function() {
logout: async function() {
store.logout();
},
},
Expand Down
9 changes: 7 additions & 2 deletions client/src/components/Login.vue
Original file line number Diff line number Diff line change
Expand Up @@ -88,13 +88,18 @@ export default {

// Update the button state
this.loggingIn = true

// clear any existing local session info
store.authenticated = false;
auth.clearToken()

try {
// Server: create a new account / log in with the provided credentials
// - returns a JWT token for the user.
// - returns a session token for the user.
const authResponse = await axios.post(apiRoute, {email, password});
// Authentication success
store.authenticated = true;
// Store the JWT from the server
// Store the token from the server
auth.setToken(authResponse.data.token);
// Local store: update the user from the API
const updatedUser = await store.fetchUser();
Expand Down
3 changes: 2 additions & 1 deletion client/src/components/Pricing.vue
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@
</div>
<p>plan and use <input type="number" v-model.number="estimatedRequests"> requests,
your bill will be <span v-text="estimatedTotalCost"></span>.
</p>
</p>
</div>
</div>
</div>
</section>
Expand Down
4 changes: 2 additions & 2 deletions client/src/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ const store = {
defaultFontSample: '',
fontSample: '',
fontSize: '40',
authenticated: auth.hasValidToken(),
authenticated: auth.loggedIn(),
email: '',
subscription: null,
source: null,
Expand Down Expand Up @@ -223,7 +223,7 @@ store.defaultFontSample = store.randomQuote();
store.fontSample = store.defaultFontSample;
// Get the Stripe key
store.getStripeKey();
// If we have a JWT stored, add a header all requests identifying the user
// If we have a session token stored, add a header all requests identifying the user
auth.setHeader();

export default store;
4 changes: 1 addition & 3 deletions config.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ module.exports = {
publicKey: process.env.STRIPE_PUBLISHABLE_KEY,
secretKey: process.env.STRIPE_SECRET_KEY,
},
// Configuration for Knex using a s
// Configuration for Knex using a sqlite3 database
database: {
client: 'sqlite3',
connection: {
Expand All @@ -45,6 +45,4 @@ module.exports = {
// Use `null` for any default values in SQLite
useNullAsDefault: true,
},
// Secret for generating JSON web tokens: this can be any very long random string
jwtSecret: process.env.JWT_SECRET,
};
90 changes: 0 additions & 90 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 0 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@
"connect-history-api-fallback": "^1.5.0",
"dotenv": "^5.0.1",
"express": "^4.16.3",
"jsonwebtoken": "^8.3.0",
"jwt-decode": "^2.2.0",
"knex": "^0.14.6",
"mysql": "^2.16.0",
"parse5": "^2.2.3",
Expand Down
61 changes: 61 additions & 0 deletions server/middleware/session.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/**
* session.js
* Stripe Billing demo. Created by Michael Glukhovsky (@mglukhovsky).
*
* This Express middleware (exported as `middleware`) checks if the incoming
* request includes a valid session token in the `Authorization` header. If
* so, the associated account and customer IDs are set in `locals`. If not,
* the request is sent to the error handler.
*
* a `getRequestToken` utility function is also exported for use in the
* `/logout` route.
*/
'use strict';

const db = require('../database');

function oneHourAgo() {
return (Date.now() / 1000) - (60 * 60);
}

function getRequestToken(req) {
// Check if an `Authorization` header was included
const header = req.headers.authorization

if (!header || !header.startsWith('Bearer')) {
// Failed: no token provided
throw new Error('No `Authorization` header provided.')
}
return header.replace(/^Bearer /, '')
}

async function middleware(req, res, next) {
try {
// Get session token and look up associated data
const token = getRequestToken(req)
const [session] = await db('sessions')
.where('token', token)
.andWhere('timestamp', '>', oneHourAgo());

if (!session) {
throw new Error(`Session not found for token: ${token}`)
}

// Update session timestamp
try {
await db('sessions').where('token', token).update('timestamp', Date.now()/1000)
} catch(e) {
// unexpected, but not fatal, so continue
}

// Success: include session data in the request
res.locals.accountId = session.accountId
res.locals.customerId = session.customerId
return next()

} catch (err) {
err.authFailed = true
return next(err)
}
}
module.exports = {middleware, getRequestToken}
Loading