Token type improvement
This commit is contained in:
		
							parent
							
								
									c6c98d36a0
								
							
						
					
					
						commit
						187172e9e7
					
				@ -2,10 +2,10 @@ import { TokenClaims } from "@common/TokenClaims";
 | 
				
			|||||||
import * as jwt from "jsonwebtoken";
 | 
					import * as jwt from "jsonwebtoken";
 | 
				
			||||||
import { computed, createAtom, IAtom, observable } from "mobx";
 | 
					import { computed, createAtom, IAtom, observable } from "mobx";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export class Token {
 | 
					export class Token<TClaims extends TokenClaims = TokenClaims> {
 | 
				
			||||||
    @observable token: string | null;
 | 
					    @observable token: string | null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @computed get claims(): TokenClaims | null {
 | 
					    @computed get claims(): TClaims | null {
 | 
				
			||||||
        if (this.token == null) {
 | 
					        if (this.token == null) {
 | 
				
			||||||
            return null;
 | 
					            return null;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
				
			|||||||
@ -1,12 +1,13 @@
 | 
				
			|||||||
import { observable } from "mobx";
 | 
					import { observable } from "mobx";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { Token } from "@client/state/Token";
 | 
					import { Token } from "@client/state/Token";
 | 
				
			||||||
 | 
					import { AccessToken, RefreshToken } from "@common/TokenClaims";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const LOCAL_STORAGE_KEY = "TokenStore";
 | 
					const LOCAL_STORAGE_KEY = "TokenStore";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export class TokenStore {
 | 
					export class TokenStore {
 | 
				
			||||||
    @observable accessToken: Token = new Token();
 | 
					    @observable accessToken: Token<AccessToken> = new Token();
 | 
				
			||||||
    @observable refreshToken: Token = new Token();
 | 
					    @observable refreshToken: Token<RefreshToken> = new Token();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    clear() {
 | 
					    clear() {
 | 
				
			||||||
        this.accessToken.token = null;
 | 
					        this.accessToken.token = null;
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,6 @@
 | 
				
			|||||||
.app {
 | 
					.app {
 | 
				
			||||||
  padding-top: 1em;
 | 
					  padding-top: 1em;
 | 
				
			||||||
 | 
					  padding-bottom: 1em;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.flex-horizontal-space-between {
 | 
					.flex-horizontal-space-between {
 | 
				
			||||||
 | 
				
			|||||||
@ -3,8 +3,14 @@ export interface BaseClaims {
 | 
				
			|||||||
    exp?: number;
 | 
					    exp?: number;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface AccessOrRefreshToken extends BaseClaims {
 | 
					export interface AccessToken extends BaseClaims {
 | 
				
			||||||
    type: "access" | "refresh";
 | 
					    type: "access";
 | 
				
			||||||
 | 
					    aud: number;
 | 
				
			||||||
 | 
					    name: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface RefreshToken extends BaseClaims {
 | 
				
			||||||
 | 
					    type: "refresh";
 | 
				
			||||||
    aud: number;
 | 
					    aud: number;
 | 
				
			||||||
    name: string;
 | 
					    name: string;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -13,4 +19,4 @@ export interface DeviceRegistrationToken extends BaseClaims {
 | 
				
			|||||||
    type: "device_reg";
 | 
					    type: "device_reg";
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type TokenClaims = AccessOrRefreshToken | DeviceRegistrationToken;
 | 
					export type TokenClaims = AccessToken | RefreshToken | DeviceRegistrationToken;
 | 
				
			||||||
 | 
				
			|||||||
@ -4,7 +4,7 @@ import { serialize} from "serializr";
 | 
				
			|||||||
import ApiError from "@common/ApiError";
 | 
					import ApiError from "@common/ApiError";
 | 
				
			||||||
import { ErrorCode } from "@common/ErrorCode";
 | 
					import { ErrorCode } from "@common/ErrorCode";
 | 
				
			||||||
import * as schema from "@common/sprinklersRpc/schema";
 | 
					import * as schema from "@common/sprinklersRpc/schema";
 | 
				
			||||||
import { AccessOrRefreshToken } from "@common/TokenClaims";
 | 
					import { AccessToken } from "@common/TokenClaims";
 | 
				
			||||||
import { verifyAuthorization } from "@server/express/authentication";
 | 
					import { verifyAuthorization } from "@server/express/authentication";
 | 
				
			||||||
import { ServerState } from "@server/state";
 | 
					import { ServerState } from "@server/state";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -12,7 +12,7 @@ export function devices(state: ServerState) {
 | 
				
			|||||||
    const router = PromiseRouter();
 | 
					    const router = PromiseRouter();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    router.get("/:deviceId", verifyAuthorization(), async (req, res) => {
 | 
					    router.get("/:deviceId", verifyAuthorization(), async (req, res) => {
 | 
				
			||||||
        const token = req.token! as AccessOrRefreshToken;
 | 
					        const token = req.token!;
 | 
				
			||||||
        const userId = token.aud;
 | 
					        const userId = token.aud;
 | 
				
			||||||
        const deviceId = req.params.deviceId;
 | 
					        const deviceId = req.params.deviceId;
 | 
				
			||||||
        const userDevice = await state.database.sprinklersDevices
 | 
					        const userDevice = await state.database.sprinklersDevices
 | 
				
			||||||
 | 
				
			|||||||
@ -10,16 +10,14 @@ import {
 | 
				
			|||||||
    TokenGrantRequest,
 | 
					    TokenGrantRequest,
 | 
				
			||||||
    TokenGrantResponse,
 | 
					    TokenGrantResponse,
 | 
				
			||||||
} from "@common/httpApi";
 | 
					} from "@common/httpApi";
 | 
				
			||||||
import { TokenClaims } from "@common/TokenClaims";
 | 
					import { AccessToken, DeviceRegistrationToken, RefreshToken, TokenClaims } from "@common/TokenClaims";
 | 
				
			||||||
import { User } from "../entities";
 | 
					import { User } from "../entities";
 | 
				
			||||||
import { ServerState } from "../state";
 | 
					import { ServerState } from "../state";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export { TokenClaims };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
declare global {
 | 
					declare global {
 | 
				
			||||||
    namespace Express {
 | 
					    namespace Express {
 | 
				
			||||||
        interface Request {
 | 
					        interface Request {
 | 
				
			||||||
            token?: TokenClaims;
 | 
					            token?: AccessToken;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -53,7 +51,9 @@ function signToken(claims: TokenClaims): Promise<string> {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function verifyToken(token: string): Promise<TokenClaims> {
 | 
					export function verifyToken<TClaims extends TokenClaims = TokenClaims>(
 | 
				
			||||||
 | 
					    token: string, type?: TClaims["type"],
 | 
				
			||||||
 | 
					): Promise<TClaims> {
 | 
				
			||||||
    return new Promise((resolve, reject) => {
 | 
					    return new Promise((resolve, reject) => {
 | 
				
			||||||
        jwt.verify(token, JWT_SECRET, {
 | 
					        jwt.verify(token, JWT_SECRET, {
 | 
				
			||||||
            issuer: ISSUER,
 | 
					            issuer: ISSUER,
 | 
				
			||||||
@ -67,14 +67,18 @@ export function verifyToken(token: string): Promise<TokenClaims> {
 | 
				
			|||||||
                    reject(err);
 | 
					                    reject(err);
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            } else {
 | 
					            } else {
 | 
				
			||||||
                resolve(decoded as any);
 | 
					                const claims: TokenClaims = decoded as any;
 | 
				
			||||||
 | 
					                if (type != null && claims.type !== type) {
 | 
				
			||||||
 | 
					                    reject(new ApiError(`Expected a "${type} token, received a "${claims.type}" token`));
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                resolve(claims as TClaims);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function generateAccessToken(user: User, secret: string): Promise<string> {
 | 
					function generateAccessToken(user: User, secret: string): Promise<string> {
 | 
				
			||||||
    const access_token_claims: TokenClaims = {
 | 
					    const access_token_claims: AccessToken = {
 | 
				
			||||||
        iss: ISSUER,
 | 
					        iss: ISSUER,
 | 
				
			||||||
        aud: user.id,
 | 
					        aud: user.id,
 | 
				
			||||||
        name: user.name,
 | 
					        name: user.name,
 | 
				
			||||||
@ -86,7 +90,7 @@ function generateAccessToken(user: User, secret: string): Promise<string> {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function generateRefreshToken(user: User, secret: string): Promise<string> {
 | 
					function generateRefreshToken(user: User, secret: string): Promise<string> {
 | 
				
			||||||
    const refresh_token_claims: TokenClaims = {
 | 
					    const refresh_token_claims: RefreshToken = {
 | 
				
			||||||
        iss: ISSUER,
 | 
					        iss: ISSUER,
 | 
				
			||||||
        aud: user.id,
 | 
					        aud: user.id,
 | 
				
			||||||
        name: user.name,
 | 
					        name: user.name,
 | 
				
			||||||
@ -98,7 +102,7 @@ function generateRefreshToken(user: User, secret: string): Promise<string> {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function generateDeviceRegistrationToken(secret: string): Promise<string> {
 | 
					function generateDeviceRegistrationToken(secret: string): Promise<string> {
 | 
				
			||||||
    const device_reg_token_claims: TokenClaims = {
 | 
					    const device_reg_token_claims: DeviceRegistrationToken = {
 | 
				
			||||||
        iss: ISSUER,
 | 
					        iss: ISSUER,
 | 
				
			||||||
        type: "device_reg",
 | 
					        type: "device_reg",
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
@ -197,7 +201,7 @@ export function verifyAuthorization(options?: Partial<VerifyAuthorizationOpts>):
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
            const token = matches[1];
 | 
					            const token = matches[1];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            req.token = await verifyToken(token);
 | 
					            req.token = await verifyToken<AccessToken>(token, "access");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (req.token.type !== opts.type) {
 | 
					            if (req.token.type !== opts.type) {
 | 
				
			||||||
                throw new ApiError(`Invalid token type "${req.token.type}", must be "${opts.type}"`,
 | 
					                throw new ApiError(`Invalid token type "${req.token.type}", must be "${opts.type}"`,
 | 
				
			||||||
 | 
				
			|||||||
@ -9,8 +9,9 @@ import * as deviceRequests from "@common/sprinklersRpc/deviceRequests";
 | 
				
			|||||||
import * as schema from "@common/sprinklersRpc/schema";
 | 
					import * as schema from "@common/sprinklersRpc/schema";
 | 
				
			||||||
import * as ws from "@common/sprinklersRpc/websocketData";
 | 
					import * as ws from "@common/sprinklersRpc/websocketData";
 | 
				
			||||||
import { User } from "@server/entities";
 | 
					import { User } from "@server/entities";
 | 
				
			||||||
import { TokenClaims, verifyToken } from "@server/express/authentication";
 | 
					import { verifyToken } from "@server/express/authentication";
 | 
				
			||||||
import { ServerState } from "@server/state";
 | 
					import { ServerState } from "@server/state";
 | 
				
			||||||
 | 
					import { AccessToken } from "@common/TokenClaims";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// tslint:disable:member-ordering
 | 
					// tslint:disable:member-ordering
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -78,19 +79,16 @@ export class WebSocketClient {
 | 
				
			|||||||
            if (!data.accessToken) {
 | 
					            if (!data.accessToken) {
 | 
				
			||||||
                throw new ws.RpcError("no token specified", ErrorCode.BadRequest);
 | 
					                throw new ws.RpcError("no token specified", ErrorCode.BadRequest);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            let decoded: TokenClaims;
 | 
					            let claims: AccessToken;
 | 
				
			||||||
            try {
 | 
					            try {
 | 
				
			||||||
                decoded = await verifyToken(data.accessToken);
 | 
					                claims = await verifyToken<AccessToken>(data.accessToken, "access");
 | 
				
			||||||
            } catch (e) {
 | 
					            } catch (e) {
 | 
				
			||||||
                throw new ws.RpcError("invalid token", ErrorCode.BadToken, e);
 | 
					                throw new ws.RpcError("invalid token", ErrorCode.BadToken, e);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            if (decoded.type !== "access") {
 | 
					            this.userId = claims.aud;
 | 
				
			||||||
                throw new ws.RpcError("not an access token", ErrorCode.BadToken);
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            this.userId = decoded.aud;
 | 
					 | 
				
			||||||
            this.user = await this.state.database.users.
 | 
					            this.user = await this.state.database.users.
 | 
				
			||||||
                findById(this.userId, { devices: true }) || null;
 | 
					                findById(this.userId, { devices: true }) || null;
 | 
				
			||||||
            log.info({ userId: decoded.aud, name: decoded.name }, "authenticated websocket client");
 | 
					            log.info({ userId: claims.aud, name: claims.name }, "authenticated websocket client");
 | 
				
			||||||
            this.subscribeBrokerConnection();
 | 
					            this.subscribeBrokerConnection();
 | 
				
			||||||
            return {
 | 
					            return {
 | 
				
			||||||
                result: "success",
 | 
					                result: "success",
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user