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