109 lines
3.5 KiB
D
109 lines
3.5 KiB
D
module auth.service;
|
|
|
|
import handy_httpd;
|
|
import handy_httpd.components.optional;
|
|
import slf4d;
|
|
|
|
import auth.model;
|
|
import auth.dao;
|
|
import handy_httpd.handlers.filtered_handler;
|
|
|
|
const SECRET = "temporary-insecure-secret"; // TODO: Load secret from application config!
|
|
|
|
/**
|
|
* Generates a new JWT access token for a user.
|
|
* Params:
|
|
* user = The user to generate the token for.
|
|
* Returns: The token.
|
|
*/
|
|
string generateAccessToken(User user) {
|
|
import jwt.jwt : Token;
|
|
import jwt.algorithms : JWTAlgorithm;
|
|
import std.datetime;
|
|
const TIMEOUT_MINUTES = 30;
|
|
Token token = new Token(JWTAlgorithm.HS512);
|
|
token.claims.aud("finnow-api");
|
|
token.claims.sub(user.username);
|
|
token.claims.exp(Clock.currTime().toUnixTime() + TIMEOUT_MINUTES * 60);
|
|
token.claims.iss("finnow-api");
|
|
return token.encode(SECRET);
|
|
}
|
|
|
|
/**
|
|
* A request filter that only permits authenticated requests to be processed.
|
|
*/
|
|
class TokenAuthenticationFilter : HttpRequestFilter {
|
|
private static const AUTH_METADATA_KEY = "AuthContext";
|
|
private immutable string secret;
|
|
|
|
this(string secret) {
|
|
this.secret = secret;
|
|
}
|
|
|
|
void apply(ref HttpRequestContext ctx, FilterChain fc) {
|
|
Optional!AuthContext optionalAuth = validateAuthContext(ctx);
|
|
if (!optionalAuth.isNull) {
|
|
ctx.metadata[AUTH_METADATA_KEY] = optionalAuth.value;
|
|
fc.doFilter(ctx); // Only continue the filter chain if a valid auth context was obtained.
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Information about the current request's authentication status.
|
|
class AuthContext {
|
|
string token;
|
|
User user;
|
|
this(string token, User user) {
|
|
this.token = token;
|
|
this.user = user;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Helper method to get the authentication context from a request context
|
|
* that was previously passed through this filter.
|
|
* Params:
|
|
* ctx = The request context to get.
|
|
* Returns: The auth context that has been set.
|
|
*/
|
|
AuthContext getAuthContext(ref HttpRequestContext ctx) {
|
|
return cast(AuthContext) ctx.metadata[TokenAuthenticationFilter.AUTH_METADATA_KEY];
|
|
}
|
|
|
|
private Optional!AuthContext validateAuthContext(ref HttpRequestContext ctx) {
|
|
import jwt.jwt : verify, Token;
|
|
import jwt.algorithms : JWTAlgorithm;
|
|
import std.typecons;
|
|
|
|
const HEADER_NAME = "Authorization";
|
|
if (!ctx.request.headers.contains(HEADER_NAME)) {
|
|
return setUnauthorized(ctx, "Missing Authorization header.");
|
|
}
|
|
|
|
string authorizationHeader = ctx.request.headers.getFirst(HEADER_NAME).orElse("");
|
|
if (authorizationHeader.length < 7 || authorizationHeader[0..7] != "Bearer ") {
|
|
return setUnauthorized(ctx, "Invalid Authorization header format. Expected bearer token.");
|
|
}
|
|
|
|
string rawToken = authorizationHeader[7..$];
|
|
try {
|
|
Token token = verify(rawToken, SECRET, [JWTAlgorithm.HS512]);
|
|
string username = token.claims.sub;
|
|
UserRepository userRepo = new FileSystemUserRepository();
|
|
Optional!User optionalUser = userRepo.findByUsername(username);
|
|
if (optionalUser.isNull) {
|
|
return setUnauthorized(ctx, "User does not exist.");
|
|
}
|
|
return Optional!AuthContext.of(new AuthContext(rawToken, optionalUser.value));
|
|
} catch (Exception e) {
|
|
warn("Failed to verify user token.", e);
|
|
return setUnauthorized(ctx, "Invalid or malformed token.");
|
|
}
|
|
}
|
|
|
|
private Optional!AuthContext setUnauthorized(ref HttpRequestContext ctx, string msg) {
|
|
ctx.response.status = HttpStatus.UNAUTHORIZED;
|
|
ctx.response.writeBodyString(msg);
|
|
return Optional!AuthContext.empty;
|
|
}
|