Validate api token on authenticated requests
Also implements logic to refresh the token if a request fails. Fixes #3
This commit is contained in:
		
							parent
							
								
									33c31a13d7
								
							
						
					
					
						commit
						3c94c25458
					
				
					 2 changed files with 84 additions and 22 deletions
				
			
		
							
								
								
									
										34
									
								
								src/app.ts
									
										
									
									
									
								
							
							
						
						
									
										34
									
								
								src/app.ts
									
										
									
									
									
								
							|  | @ -9,6 +9,18 @@ import fetch, { Response as FetchResponse } from "node-fetch"; | |||
| import db from "./db"; | ||||
| import errorHandler from "./errors"; | ||||
| 
 | ||||
| // Ensure that any API token we have is valid - if not, destroy the session,
 | ||||
| // logging out the user. Should be called before checking whether a user is
 | ||||
| // logged in.
 | ||||
| async function validateApiToken(session: Express.Session) { | ||||
| 	if (session.tokenpair) { | ||||
| 		console.log(session.tokenpair) | ||||
| 	} | ||||
| 	if (session.tokenpair && ! (await twitch.isApiTokenValid(session.tokenpair))) { | ||||
| 		session.destroy(()=>{}); | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| const app = express(); | ||||
| app.use(express.static('public')); | ||||
| app.use(express.urlencoded({extended: false})); | ||||
|  | @ -27,6 +39,7 @@ app.get("/api/getRequests", async (request, response) => { | |||
| 		throw new Error ("Missing request.session") | ||||
| 	} | ||||
| 	var requestCount = ( request.query.count ? parseInt(request.query.count as string, 10) : 5 ); | ||||
| 	await validateApiToken(request.session); | ||||
| 	if (request.session.user) { | ||||
| 		requests.getRequestsVoted(requestCount,request.session.user.id).then((val: Array<any>) => response.send(val)) | ||||
| 			.catch((e: any) => errorHandler(request,response,e)); | ||||
|  | @ -41,6 +54,7 @@ app.get("/api/getAllRequests", async (request, response) => { | |||
| 		throw new Error ("Missing request.session") | ||||
| 	} | ||||
| 	var requestCount = ( request.query.count ? parseInt(request.query.count as string, 10) : 5 ); | ||||
| 	await validateApiToken(request.session); | ||||
| 	if (request.session.user) { | ||||
| 		requests.getAllRequestsVoted(requestCount,request.session.user.id).then((val: Array<any>) => response.send(val)) | ||||
| 			.catch((e: any) => errorHandler(request,response,e)); | ||||
|  | @ -52,6 +66,7 @@ app.get("/api/getAllRequests", async (request, response) => { | |||
| 
 | ||||
| app.post("/api/addRequest", async (request, response) => { | ||||
| 	response.type('text/plain'); | ||||
| 	if (request.session) await validateApiToken(request.session); | ||||
| 	if (!request.session || !request.session.user) { | ||||
| 		response.status(401); | ||||
| 		response.send("Must be logged in"); | ||||
|  | @ -71,7 +86,7 @@ app.post("/api/addRequest", async (request, response) => { | |||
| 	.catch((e: any) => errorHandler(request,response,e)); | ||||
| }); | ||||
| 
 | ||||
| app.post("/api/updateRequestState", async (request, response) => { | ||||
| app.post("/api/updateRequestState", async (request, response) => { // TODO: Streamer auth
 | ||||
| 	response.type('text/plain'); | ||||
| 	if (!request.body.url) { | ||||
| 		response.status(400); | ||||
|  | @ -92,7 +107,7 @@ app.post("/api/updateRequestState", async (request, response) => { | |||
| 	.catch((e: any) => errorHandler(request,response,e)); | ||||
| }); | ||||
| 
 | ||||
| app.post("/api/updateRequestScore", async (request, response) => { | ||||
| app.post("/api/updateRequestScore", async (request, response) => { // TODO: Streamer auth
 | ||||
| 	response.type('text/plain'); | ||||
| 	if (!request.body.url) { | ||||
| 		response.status(400); | ||||
|  | @ -113,7 +128,7 @@ app.post("/api/updateRequestScore", async (request, response) => { | |||
| 	.catch((e: any) => errorHandler(request,response,e)); | ||||
| }); | ||||
| 
 | ||||
| app.post("/api/deleteRequest", async (request, response) => { | ||||
| app.post("/api/deleteRequest", async (request, response) => { // TODO: Streamer auth
 | ||||
| 	response.type('text/plain'); | ||||
| 	if (!request.body.url) { | ||||
| 		response.status(400); | ||||
|  | @ -130,6 +145,7 @@ app.post("/api/deleteRequest", async (request, response) => { | |||
| 
 | ||||
| app.post("/api/addVote", async (request,response) => { | ||||
| 	response.type('text/plain'); | ||||
| 	if (request.session) await validateApiToken(request.session); | ||||
| 	if (!request.session || !request.session.user) { | ||||
| 		response.status(401); | ||||
| 		response.send("Must be logged in"); | ||||
|  | @ -151,6 +167,7 @@ app.post("/api/addVote", async (request,response) => { | |||
| 
 | ||||
| app.post("/api/deleteVote", async (request,response) => { | ||||
| 	response.type('text/plain'); | ||||
| 	if (request.session) await validateApiToken(request.session); | ||||
| 	if (!request.session || !request.session.user) { | ||||
| 		response.status(401); | ||||
| 		response.send("Must be logged in"); | ||||
|  | @ -183,12 +200,12 @@ app.get("/callback", async (request, response) => { | |||
| 		code: authcode, | ||||
| 		grant_type: "authorization_code", | ||||
| 		redirect_uri: `${config.urlPrefix}/callback` | ||||
| 	})}).then((res: FetchResponse) => res.json() as Promise<twitch.TokenResponse>) | ||||
| 	})}).then((res: FetchResponse) => res.json() as Promise<twitch.TokenPair>) | ||||
| 	.catch((e: any) => errorHandler(request,response,e)); | ||||
| 	if (typeof request.session == 'undefined') throw new Error('Session is undefined'); | ||||
| 	if (typeof tokenResponse == 'undefined') throw new Error('tokenResponse is undefined'); | ||||
| 	request.session.tokenpair = { access_token: tokenResponse.access_token, refresh_token: tokenResponse.refresh_token }; | ||||
| 	request.session.user = (await twitch.apiRequest(request.session.tokenpair,"GET","/users")).data[0]; | ||||
| 	request.session.user = (await twitch.apiRequest(request.session.tokenpair,"/users")).data[0]; | ||||
| 
 | ||||
| 	const updateUserQuery = { | ||||
| 		name: "updateUser", | ||||
|  | @ -202,10 +219,8 @@ app.get("/callback", async (request, response) => { | |||
| 
 | ||||
| // Frontend templates
 | ||||
| app.get("/", async (request, response) => { | ||||
| 	if (!request.session) { | ||||
| 		throw new Error ("Missing request.session") | ||||
| 	} | ||||
| 	if (!request.session.user) { | ||||
| 	if (request.session) await validateApiToken(request.session); | ||||
| 	if (!request.session || !request.session.user) { | ||||
| 		response.render('main.eta', { | ||||
| 			loggedIn: false, | ||||
| 			clientId: config.twitchClientId, | ||||
|  | @ -223,7 +238,6 @@ app.get("/", async (request, response) => { | |||
| // Logout
 | ||||
| app.get ("/logout", async (request, response) => request.session!.destroy(() => response.redirect(307, '/'))); | ||||
| 
 | ||||
| 
 | ||||
| app.listen(config.port, () => { | ||||
|   console.log(`Listening on port ${config.port}`); | ||||
| }); | ||||
|  |  | |||
|  | @ -1,25 +1,73 @@ | |||
| import * as config from "./config"; | ||||
| import fetch, { Response as FetchResponse } from "node-fetch"; | ||||
| 
 | ||||
| export interface TokenResponse { | ||||
| 	access_token: string; | ||||
| 	refresh_token: string; | ||||
| 	token_type: string; | ||||
| 	expires_in: number; | ||||
| } | ||||
| 
 | ||||
| export interface TokenPair { | ||||
| 	access_token: string; | ||||
| 	refresh_token: string; | ||||
| } | ||||
| 
 | ||||
| export async function apiRequest(tokens: TokenPair, method: string, endpoint: string): Promise<any>; | ||||
| export async function apiRequest(tokens: TokenPair, method: string, endpoint: string, query: string): Promise<any>; | ||||
| export async function apiRequest(tokens: TokenPair, method: string, endpoint: string, query?: string,) { | ||||
| // Refresh the API token. Returns true on success and false on failure.
 | ||||
| async function refreshApiToken(tokens: TokenPair): Promise<boolean> { | ||||
| 	return fetch("https://id.twitch.tv/oauth2/token", { | ||||
| 		method: 'POST', | ||||
| 		body: new URLSearchParams({ | ||||
| 			client_id: config.twitchClientId, | ||||
| 			client_secret: config.twitchSecret, | ||||
| 			grant_type: "refresh_token", | ||||
| 			refresh_token: tokens.refresh_token | ||||
| 		}) | ||||
| 	}).then(async (res: FetchResponse) => { | ||||
| 		if (res.status == 200) { | ||||
| 			var data = await (res.json() as Promise<TokenPair>); | ||||
| 			console.log(data) | ||||
| 			tokens.access_token = data.access_token; | ||||
| 			tokens.refresh_token = data.refresh_token; | ||||
| 			return true; | ||||
| 		} else { | ||||
| 			return false; | ||||
| 		} | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
| // Send an API request. On success, return the specified data. On failure,
 | ||||
| // attempt to refresh the API token and retry
 | ||||
| export async function apiRequest(tokens: TokenPair, endpoint: string): Promise <any> { | ||||
| 	var headers = { | ||||
| 		"Authorization": "Bearer " + tokens.access_token, | ||||
| 		"Client-ID": config.twitchClientId | ||||
| 	}; | ||||
| 	return fetch("https://api.twitch.tv/helix" + endpoint, { method: method, headers: headers}) | ||||
| 		.then(async (res: FetchResponse) => res.json()); | ||||
| 	return fetch("https://api.twitch.tv/helix" + endpoint, { headers: headers }) | ||||
| 		.then((res: FetchResponse) => { | ||||
| 			if (res.status == 200) { | ||||
| 				return res.json(); | ||||
| 			} else { | ||||
| 				if (refreshApiToken(tokens)) { | ||||
| 					return fetch("https://api.twitch.tv/helix" + endpoint, { headers: headers }) | ||||
| 						.then((res: FetchResponse) => { | ||||
| 							if (res.status == 200) { | ||||
| 								return res.json(); | ||||
| 							} else { | ||||
| 								return false; | ||||
| 							} | ||||
| 					}) | ||||
| 				} else { | ||||
| 					return false; | ||||
| 				} | ||||
| 			} | ||||
| 		}) | ||||
| } | ||||
| 
 | ||||
| // Check if API token is valid. Checks Twitch's OAuth validation endpoint. If
 | ||||
| // success, return true. If failure, return the result of attempting to refresh
 | ||||
| // the API token.
 | ||||
| export async function isApiTokenValid(tokens: TokenPair) { | ||||
| 	return fetch("https://id.twitch.tv/oauth2/validate", { | ||||
| 		headers: {'Authorization': `OAuth ${tokens.access_token}`} | ||||
| 	}).then((res: FetchResponse) => { | ||||
| 		if (res.status == 200) { | ||||
| 			return true; | ||||
| 		} else { | ||||
| 			return refreshApiToken(tokens); | ||||
| 		} | ||||
| 	}) | ||||
| } | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue