You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
132 lines
3.4 KiB
132 lines
3.4 KiB
import * as Express from "express"; |
|
import Router from "express-promise-router"; |
|
import * as jwt from "jsonwebtoken"; |
|
|
|
import ApiError from "@common/ApiError"; |
|
import { ErrorCode } from "@common/ErrorCode"; |
|
import { |
|
TokenGrantPasswordRequest, |
|
TokenGrantRefreshRequest, |
|
TokenGrantRequest, |
|
TokenGrantResponse |
|
} from "@common/httpApi"; |
|
import * as tok from "@common/TokenClaims"; |
|
import { User } from "@server/entities"; |
|
import { ServerState } from "@server/state"; |
|
|
|
const JWT_SECRET = process.env.JWT_SECRET!; |
|
if (!JWT_SECRET) { |
|
throw new Error("Must specify JWT_SECRET environment variable"); |
|
} |
|
|
|
const ISSUER = "sprinklers3"; |
|
|
|
const ACCESS_TOKEN_LIFETIME = 30 * 60; // 30 minutes |
|
const REFRESH_TOKEN_LIFETIME = 7 * 24 * 60 * 60; // 7 days |
|
|
|
function signToken( |
|
claims: tok.TokenClaimTypes, |
|
opts?: jwt.SignOptions |
|
): Promise<string> { |
|
const options: jwt.SignOptions = { |
|
issuer: ISSUER, |
|
...opts |
|
}; |
|
return new Promise((resolve, reject) => { |
|
jwt.sign(claims, JWT_SECRET, options, (err: Error, encoded: string) => { |
|
if (err) { |
|
reject(err); |
|
} else { |
|
resolve(encoded); |
|
} |
|
}); |
|
}); |
|
} |
|
|
|
export function verifyToken< |
|
TClaims extends tok.TokenClaimTypes = tok.TokenClaimTypes |
|
>(token: string, type?: TClaims["type"]): Promise<TClaims & tok.BaseClaims> { |
|
return new Promise((resolve, reject) => { |
|
jwt.verify( |
|
token, |
|
JWT_SECRET, |
|
{ |
|
issuer: ISSUER |
|
}, |
|
(err, decoded) => { |
|
if (err) { |
|
if (err.name === "TokenExpiredError") { |
|
reject( |
|
new ApiError( |
|
"The specified token is expired", |
|
ErrorCode.BadToken, |
|
err |
|
) |
|
); |
|
} else if (err.name === "JsonWebTokenError") { |
|
reject(new ApiError("Invalid token", ErrorCode.BadToken, err)); |
|
} else { |
|
reject(err); |
|
} |
|
} else { |
|
const claims: tok.TokenClaims = decoded as any; |
|
if (type != null && claims.type !== type) { |
|
reject( |
|
new ApiError( |
|
`Expected a "${type}" token, received a "${claims.type}" token`, |
|
ErrorCode.BadToken |
|
) |
|
); |
|
} |
|
resolve(claims as TClaims & tok.BaseClaims); |
|
} |
|
} |
|
); |
|
}); |
|
} |
|
|
|
export function generateAccessToken(user: User): Promise<string> { |
|
const accessTokenClaims: tok.AccessToken = { |
|
aud: user.id, |
|
name: user.name, |
|
type: "access" |
|
}; |
|
|
|
return signToken(accessTokenClaims, { expiresIn: ACCESS_TOKEN_LIFETIME }); |
|
} |
|
|
|
export function generateRefreshToken(user: User): Promise<string> { |
|
const refreshTokenClaims: tok.RefreshToken = { |
|
aud: user.id, |
|
name: user.name, |
|
type: "refresh" |
|
}; |
|
|
|
return signToken(refreshTokenClaims, { expiresIn: REFRESH_TOKEN_LIFETIME }); |
|
} |
|
|
|
export function generateDeviceRegistrationToken(): Promise<string> { |
|
const deviceRegTokenClaims: tok.DeviceRegistrationToken = { |
|
type: "device_reg" |
|
}; |
|
return signToken(deviceRegTokenClaims); |
|
} |
|
|
|
export function generateDeviceToken( |
|
id: number, |
|
deviceId: string |
|
): Promise<string> { |
|
const deviceTokenClaims: tok.DeviceToken = { |
|
type: "device", |
|
aud: deviceId, |
|
id |
|
}; |
|
return signToken(deviceTokenClaims); |
|
} |
|
|
|
export function generateSuperuserToken(): Promise<string> { |
|
const superuserClaims: tok.SuperuserToken = { |
|
type: "superuser" |
|
}; |
|
return signToken(superuserClaims); |
|
}
|
|
|