Browse Source

Token type improvement

update-deps
Alex Mikhalev 7 years ago
parent
commit
187172e9e7
  1. 4
      client/state/Token.ts
  2. 5
      client/state/TokenStore.ts
  3. 1
      client/styles/app.scss
  4. 12
      common/TokenClaims.ts
  5. 4
      server/express/api/devices.ts
  6. 24
      server/express/authentication.ts
  7. 14
      server/sprinklersRpc/websocketServer.ts

4
client/state/Token.ts

@ -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;
} }

5
client/state/TokenStore.ts

@ -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
client/styles/app.scss

@ -1,5 +1,6 @@
.app { .app {
padding-top: 1em; padding-top: 1em;
padding-bottom: 1em;
} }
.flex-horizontal-space-between { .flex-horizontal-space-between {

12
common/TokenClaims.ts

@ -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
server/express/api/devices.ts

@ -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

24
server/express/authentication.ts

@ -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}"`,

14
server/sprinklersRpc/websocketServer.ts

@ -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…
Cancel
Save