module auth.tokens; import slf4d; import secured.rsa; import secured.util; import std.datetime; import std.base64; import std.file; import asdf : serializeToJson, deserialize; import handy_http_primitives : Optional, HttpStatusException, HttpStatus; import streams : Either; const TOKEN_EXPIRATION = minutes(60); /** * Definition of the token's payload. */ private struct TokenData { string username; string issuedAt; } /** * Definition for the entire token JSON object, including the payload, and a * signature of the payload generated with the server's private key. */ private struct TokenObject { /// The token's data. TokenData data; /// The base64-encoded cryptographic signature of `data`. string sig; } /** * Generates a new token for the given user. * Params: * username = The username to generate the token for. * Returns: A new token that the user can provide to authenticate requests. */ string generateToken(in string username) { auto data = TokenData(username, Clock.currTime(UTC()).toISOExtString()); RSA rsa = getPrivateKey(); try { string dataJson = serializeToJson(data); ubyte[] signature = rsa.sign(cast(ubyte[]) dataJson); TokenObject obj = TokenObject(data, Base64.encode(signature)); string jsonStr = serializeToJson(obj); return Base64.encode(cast(ubyte[]) jsonStr); } catch (CryptographicException e) { error("Failed to sign token data.", e); throw new HttpStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Failed to generate token."); } } /// Possible errors that can occur when verifying a token. enum TokenVerificationFailure { InvalidSignature, Expired } /** * Result of token verification, which is the user's username if the token is * valid, or a failure reason if not. */ alias TokenVerificationResult = Either!(string, "username", TokenVerificationFailure, "failure"); /** * Verifies a token and returns the username, if it's valid. * Params: * token = The token to verify. * Returns: A token verification result. */ TokenVerificationResult verifyToken(in string token) { string jsonStr = cast(string) Base64.decode(cast(ubyte[]) token); TokenObject decodedToken = deserialize!TokenObject(jsonStr); string dataJson = serializeToJson(decodedToken.data); ubyte[] signature = Base64.decode(decodedToken.sig); RSA rsa = getPrivateKey(); if (!rsa.verify(cast(ubyte[]) dataJson, signature)) { warnF!"Failed to verify token signature for user: %s"(decodedToken.data.username); return TokenVerificationResult(TokenVerificationFailure.InvalidSignature); } // We have verified the signature, so now we can check various properties of the token. // Check that the token is not expired. SysTime issuedAt = SysTime.fromISOExtString(decodedToken.data.issuedAt, UTC()); SysTime now = Clock.currTime(UTC()); Duration diff = now - issuedAt; if (diff > TOKEN_EXPIRATION) { warnF!"Token for user %s has expired."(decodedToken.data.username); return TokenVerificationResult(TokenVerificationFailure.Expired); } return TokenVerificationResult(decodedToken.data.username); } private RSA getPrivateKey() { ubyte[] pkData = cast(ubyte[]) readText("test-key"); return new RSA(pkData, null); }