Skip to content

Commit 0e62ca6

Browse files
authored
Merge pull request #372 from developmentseed/fix/token-validation
Token Validation for API calls, fix #354
2 parents fb21261 + bc876d9 commit 0e62ca6

File tree

10 files changed

+98
-17
lines changed

10 files changed

+98
-17
lines changed

.env.test

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@ NEXTAUTH_URL=http://127.0.0.1:3000
22
NEXTAUTH_SECRET=next-auth-cypress-secret
33
DATABASE_URL=postgres://postgres:postgres@localhost:5434/osm-teams-test
44
TESTING=true
5-
LOG_LEVEL=silent
5+
LOG_LEVEL=silent
6+
AUTH_URL=http://127.0.0.1:3000

next.config.js

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,21 +11,14 @@ module.exports = {
1111
process.env.OSM_API ||
1212
process.env.OSM_DOMAIN ||
1313
'https://www.openstreetmap.org',
14-
OSM_HYDRA_ID: process.env.OSM_HYDRA_ID || 'manage',
15-
OSM_HYDRA_SECRET: process.env.OSM_HYDRA_SECRET || 'manage-secret',
16-
OSM_CONSUMER_KEY: process.env.OSM_CONSUMER_KEY,
17-
OSM_CONSUMER_SECRET: process.env.OSM_CONSUMER_SECRET,
18-
HYDRA_TOKEN_HOST: process.env.HYDRA_TOKEN_HOST || 'http://localhost:4444',
19-
HYDRA_TOKEN_PATH: process.env.HYDRA_TOKEN_PATH || '/oauth2/token',
20-
HYDRA_AUTHZ_HOST: process.env.HYDRA_AUTHZ_HOST || 'http://localhost:4444',
21-
HYDRA_AUTHZ_PATH: process.env.HYDRA_AUTHZ_PATH || '/oauth2/auth',
22-
HYDRA_ADMIN_HOST: process.env.HYDRA_ADMIN_HOST || 'http://localhost:4445',
2314
},
2415
basePath: process.env.BASE_PATH || '',
2516
env: {
2617
APP_URL: process.env.APP_URL || vercelUrl || 'http://127.0.0.1:3000',
2718
OSM_NAME: process.env.OSM_NAME || 'OSM',
2819
BASE_PATH: process.env.BASE_PATH || '',
20+
HYDRA_URL: process.env.HYDRA_URL || 'https://auth.mapping.team/hyauth',
21+
AUTH_URL: process.env.AUTH_URL || 'https://auth.mapping.team',
2922
},
3023
eslint: {
3124
dirs: [

src/middlewares/base-handler.js

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import nc from 'next-connect'
22
import logger from '../lib/logger'
33
import { getToken } from 'next-auth/jwt'
4+
import Boom from '@hapi/boom'
45

56
/**
67
* This file contains the base handler to be used in all API routes.
@@ -57,9 +58,40 @@ export function createBaseHandler() {
5758

5859
// Add session to request
5960
baseHandler.use(async (req, res, next) => {
60-
const token = await getToken({ req })
61-
if (token) {
62-
req.session = { user_id: token.userId || token.sub }
61+
/** Handle authorization using either Bearer token auth or
62+
* using the next-auth session
63+
*/
64+
if (req.headers.authorization) {
65+
// introspect the token
66+
const [type, token] = req.headers.authorization.split(' ')
67+
if (type !== 'Bearer') {
68+
throw Boom.badRequest(
69+
'Authorization scheme not supported. Only Bearer scheme is supported'
70+
)
71+
} else {
72+
const result = await fetch(`${process.env.AUTH_URL}/api/introspect`, {
73+
method: 'POST',
74+
headers: {
75+
Accept: 'application/json',
76+
'Content-Type': 'application/json',
77+
},
78+
body: JSON.stringify({
79+
token: token,
80+
}),
81+
}).then((response) => {
82+
return response.json()
83+
})
84+
if (result && result.active) {
85+
req.session = { user_id: result.sub }
86+
} else {
87+
throw Boom.badRequest('Invalid token')
88+
}
89+
}
90+
} else {
91+
const token = await getToken({ req })
92+
if (token) {
93+
req.session = { user_id: token.userId || token.sub }
94+
}
6395
}
6496
next()
6597
})
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import Boom from '@hapi/boom'
2+
3+
/**
4+
* Authenticated
5+
*
6+
* To view this route you must be authenticated
7+
*
8+
* @returns {Promise<boolean>}
9+
*/
10+
export default async function isAuthenticated(req, res, next) {
11+
const userId = req.session?.user_id
12+
13+
// Must be owner or manager
14+
if (!userId) {
15+
throw Boom.unauthorized()
16+
} else {
17+
next()
18+
}
19+
}

src/pages/api/auth/[...nextauth].js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,7 @@ export const authOptions = {
99
id: 'osm-teams',
1010
name: 'OSM Teams',
1111
type: 'oauth',
12-
wellKnown:
13-
'https://auth.mapping.team/hyauth/.well-known/openid-configuration',
12+
wellKnown: `${process.env.HYDRA_URL}/.well-known/openid-configuration`,
1413
authorization: { params: { scope: 'openid offline' } },
1514
idToken: true,
1615
async profile(profile) {

src/pages/api/introspect.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
const { decode } = require('next-auth/jwt')
2+
/**
3+
* !! This function is only used for testing
4+
* purposes, it mocks the hydra access token introspection
5+
*/
6+
export default async function handler(req, res) {
7+
if (req.method === 'POST') {
8+
// Process a POST request
9+
const { token } = req.body
10+
const decodedToken = await decode({
11+
token,
12+
secret: process.env.NEXT_AUTH_SECRET,
13+
})
14+
15+
const result = {
16+
active: true,
17+
sub: decodedToken.userId,
18+
}
19+
20+
res.status(200).json(result)
21+
} else {
22+
res.status(400)
23+
}
24+
}

src/pages/api/my/teams.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as Yup from 'yup'
22
import Team from '../../../models/team'
33
import { createBaseHandler } from '../../../middlewares/base-handler'
44
import { validate } from '../../../middlewares/validation'
5+
import isAuthenticated from '../../../middlewares/can/authenticated'
56

67
const handler = createBaseHandler()
78

@@ -26,6 +27,7 @@ const handler = createBaseHandler()
2627
* $ref: '#/components/schemas/ArrayOfTeams'
2728
*/
2829
handler.get(
30+
isAuthenticated,
2931
validate({
3032
query: Yup.object({
3133
page: Yup.number().min(0).integer(),

tests/api/team-api.test.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ const { resetDb, disconnectDb } = require('../utils/db-helpers')
44
const createAgent = require('../utils/create-agent')
55

66
let user1Agent
7+
let user1HttpAgent
78
test.before(async () => {
89
await resetDb()
910
user1Agent = await createAgent({ id: 1 })
11+
user1HttpAgent = await createAgent({ id: 1, http: true })
1012
})
1113

1214
test.after.always(disconnectDb)
@@ -252,7 +254,13 @@ test('get my teams list', async (t) => {
252254
data: teams,
253255
},
254256
} = await user1Agent.get(`/api/my/teams`).expect(200)
257+
255258
t.is(total, 2)
256259
t.is(teams[0].name, 'Team 1')
257260
t.is(teams[1].name, 'Team 2')
261+
262+
const httpApiResponse = await user1HttpAgent.get(`/api/my/teams`).expect(200)
263+
264+
// Has to be the same
265+
t.deepEqual(httpApiResponse.body.data, teams)
258266
})

tests/utils/create-agent.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
const getSessionToken = require('./get-session-token')
22

3-
async function createAgent(user) {
3+
async function createAgent(user, http = false) {
44
const agent = require('supertest').agent('http://localhost:3000')
55

66
if (user) {
77
const encryptedToken = await getSessionToken(
88
user,
99
process.env.NEXTAUTH_SECRET
1010
)
11+
if (http) {
12+
agent.set('Authorization', `Bearer ${encryptedToken}`)
13+
}
1114
agent.set('Cookie', [`next-auth.session-token=${encryptedToken}`])
1215
}
1316

tests/utils/get-session-token.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ async function getSessionToken(userObj, secret) {
55
const token = { ...userObj, userId: userObj.id }
66

77
// Function logic derived from https://github.com/nextauthjs/next-auth/blob/5c1826a8d1f8d8c2d26959d12375704b0a693bfc/packages/next-auth/src/jwt/index.ts#L113-L121
8-
const encryptionSecret = await await hkdf(
8+
const encryptionSecret = await hkdf(
99
'sha256',
1010
secret,
1111
'',

0 commit comments

Comments
 (0)