Skip to content
This repository has been archived by the owner on May 22, 2021. It is now read-only.

Commit

Permalink
Merge pull request #35 from E-Edu/issue/3
Browse files Browse the repository at this point in the history
Issue/3
  • Loading branch information
steve-hb authored Mar 27, 2020
2 parents e7b4b06 + 25a8257 commit e25372a
Show file tree
Hide file tree
Showing 7 changed files with 327 additions and 1 deletion.
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ dependencies {
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
compile 'com.jcabi:jcabi-manifests:0.7.5'
implementation 'com.auth0:java-jwt:3.10.1'

/* GraphQL */
implementation 'com.graphql-java-kickstart:graphql-spring-boot-starter:7.0.0'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,13 @@ public class JwtResultDTO {

@NotNull @NotEmpty @NotBlank
private String token;
@NotNull
private JwtStatus status;

public enum JwtStatus {
INVALID,
EXPIRED,
VERIFIED
}

}
Original file line number Diff line number Diff line change
@@ -1,17 +1,82 @@
package de.themorpheus.edu.gateway.graphql.resolver.mutation.user;

import de.themorpheus.edu.gateway.graphql.dto.user.JwtRequestDTO;
import de.themorpheus.edu.gateway.graphql.dto.user.JwtResultDTO;
import de.themorpheus.edu.gateway.graphql.dto.user.UserAuthDTO;
import de.themorpheus.edu.gateway.graphql.dto.user.UserAuthResultDTO;
import org.springframework.stereotype.Component;
import javax.validation.Valid;
import de.themorpheus.edu.gateway.graphql.resolver.util.HeaderUtil;
import de.themorpheus.edu.gateway.graphql.resolver.util.JsonWebToken;
import de.themorpheus.edu.gateway.graphql.resolver.util.RefreshToken;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import graphql.kickstart.tools.GraphQLMutationResolver;
import graphql.schema.DataFetchingEnvironment;

@Component
public class AuthenticationResolver implements GraphQLMutationResolver {

public UserAuthResultDTO authenticate(@Valid UserAuthDTO userAuth, DataFetchingEnvironment environment) {
return new UserAuthResultDTO(UserAuthResultDTO.UserAuthResultType.SUCCESS);
Optional<String> optionalDeviceId = HeaderUtil.findHeader(environment, HeaderUtil.DEVICE_ID);

deviceId: {
HeaderUtil.setCookie(
environment,
Map.of(
HeaderUtil.DEVICE_ID, optionalDeviceId.orElse("EXAMPLE_DEVICE_COOKIE"),
//HeaderUtil.CookieOption.SAME_SITE.getValue(), "Strict" //TODO: Activate
HeaderUtil.CookieOption.SAME_SITE.getValue(), "None"
),
HeaderUtil.CookieOption.SECURE,
HeaderUtil.CookieOption.HTTP_ONLY
);
}

//TODO: Backend authentication
UserAuthResultDTO.UserAuthResultType resultType = UserAuthResultDTO.UserAuthResultType.SUCCESS;
UUID userId = UUID.randomUUID();

if (resultType == UserAuthResultDTO.UserAuthResultType.SUCCESS) {
refreshToken: {
HeaderUtil.setCookie(
environment,
Map.of(
HeaderUtil.REFRESH_TOKEN, RefreshToken.generate(userId),
HeaderUtil.CookieOption.SAME_SITE.getValue(), "None" //TODO: Strict
),
HeaderUtil.CookieOption.SECURE,
HeaderUtil.CookieOption.HTTP_ONLY
);
}
}

return new UserAuthResultDTO(resultType);
}

public JwtResultDTO jwt(@Valid JwtRequestDTO requestDTO, DataFetchingEnvironment environment) {
// Get cookie
String refreshTokenCookie = HeaderUtil.getCookie(environment, HeaderUtil.REFRESH_TOKEN);
if (refreshTokenCookie == null) return new JwtResultDTO(null, JwtResultDTO.JwtStatus.INVALID); //TODO: 403 Forbidden

// Verify refresh token
RefreshToken.VerificationResult result = RefreshToken.verify(refreshTokenCookie);

// Malformed or invalid
if (result.getStatus() == RefreshToken.VerificationStatus.INVALID ||
result.getStatus() == RefreshToken.VerificationStatus.MALFORMED
) return new JwtResultDTO(null, JwtResultDTO.JwtStatus.INVALID); //TODO: 403 Forbidden

// Expired
if (result.getStatus() == RefreshToken.VerificationStatus.EXPIRED)
return new JwtResultDTO(null, JwtResultDTO.JwtStatus.EXPIRED);

UUID userId = result.getUserId();

// Create jwt
String jwt = JsonWebToken.generate(userId);
return new JwtResultDTO(jwt, JwtResultDTO.JwtStatus.VERIFIED);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package de.themorpheus.edu.gateway.graphql.resolver.util;

import lombok.Getter;
import lombok.RequiredArgsConstructor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Enumeration;
import java.util.Map;
import java.util.Optional;
import graphql.schema.DataFetchingEnvironment;
import graphql.servlet.context.DefaultGraphQLServletContext;

public class HeaderUtil {

public static final String DEVICE_ID = "device_id";
public static final String REFRESH_TOKEN = "refresh_token";

public static final String SET_COOKIE = "set-cookie";
public static final String COOKIE = "cookie";

public static Optional<String> findHeader(DataFetchingEnvironment environment, String header) {
DefaultGraphQLServletContext context = environment.getContext();
HttpServletRequest request = context.getHttpServletRequest();
Enumeration<String> headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
String currentHeader = headerNames.nextElement();
if (currentHeader.equalsIgnoreCase(header)) return Optional.of(request.getHeader(currentHeader));
}

return Optional.empty();
}

public static void setHeader(DataFetchingEnvironment environment, String header, String value) {
DefaultGraphQLServletContext context = environment.getContext();
HttpServletResponse response = context.getHttpServletResponse();
response.setHeader(header, value);
}

public static void setCookie(DataFetchingEnvironment environment, Map<String, String> cookies, CookieOption... options) {
DefaultGraphQLServletContext context = environment.getContext();
HttpServletResponse response = context.getHttpServletResponse();
StringBuilder value = new StringBuilder();
cookies.forEach((cookieKey, cookieValue) -> {
value.append(cookieKey);
value.append('=');
value.append(cookieValue);
value.append(';');
});

for (CookieOption option : options) {
if (option.isValueRequired()) throw new IllegalArgumentException("Consider passing this option via 'cookies': " + option);
value.append(option.getValue());
value.append(';');
}

response.setHeader(SET_COOKIE, value.toString());
}

public static String getCookie(DataFetchingEnvironment environment, String key) {
Optional<String> cookieOptional = HeaderUtil.findHeader(environment, COOKIE);
if (!cookieOptional.isPresent()) return null;

String[] parts = cookieOptional.get().split(";");

for (String part : parts)
if (part.startsWith(key) && part.contains("="))
return part.split("=")[1];

return null;
}

@RequiredArgsConstructor
public enum CookieOption {
MAX_AGE(true, "Max-Age"),
EXPIRES(true, "Expires"),
PATH(true, "Path"),
DOMAIN(true, "Domain"),
SAME_SITE(true, "SameSite"),
SECURE(false, "Secure"),
HTTP_ONLY(false, "HttpOnly");

@Getter private final boolean valueRequired;
@Getter private final String value;

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package de.themorpheus.edu.gateway.graphql.resolver.util;

import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.SignatureVerificationException;
import com.auth0.jwt.interfaces.DecodedJWT;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import java.security.SecureRandom;
import java.time.Duration;
import java.time.Instant;
import java.util.Base64;
import java.util.Date;
import java.util.UUID;

public class JsonWebToken {

private static final SecureRandom RANDOM = new SecureRandom();

private static final int RANDOM_SIZE = 128;
private static final String SECRET = "nrj5m5sui65sdu";
private static final Algorithm ALGORITHM = Algorithm.HMAC512(SECRET);

public static String generate(UUID userId) {
byte[] rand = new byte[RANDOM_SIZE];
RANDOM.nextBytes(rand);

return JWT.create()
.withIssuer("e-edu")
.withSubject("jwt")
.withClaim("userId", userId.toString())
.withClaim("rand", Base64.getEncoder().encodeToString(rand))
.withIssuedAt(Date.from(Instant.now()))
.withExpiresAt(Date.from(Instant.now().plus(Duration.ofMinutes(15))))
.sign(ALGORITHM);
}

public static VerificationResult verify(String jwt) {
DecodedJWT decodedJWT = JWT.decode(jwt);
if (decodedJWT.getExpiresAt().after(Date.from(Instant.now())))
return new VerificationResult(VerificationStatus.EXPIRED, null);

try {
ALGORITHM.verify(decodedJWT);
} catch (SignatureVerificationException ignored) {
return new VerificationResult(VerificationStatus.INVALID, null);
}

UUID userId;
try {
userId = UUID.fromString(decodedJWT.getClaim("userId").asString());
} catch (IllegalArgumentException ignored) {
return new VerificationResult(VerificationStatus.MALFORMED, null);
}

return new VerificationResult(VerificationStatus.VERIFIED, userId);
}

@Data
@RequiredArgsConstructor
public static class VerificationResult {

private final VerificationStatus status;
private final UUID userId;

}

public enum VerificationStatus {
VERIFIED, INVALID, EXPIRED, MALFORMED
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package de.themorpheus.edu.gateway.graphql.resolver.util;

import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.exceptions.SignatureVerificationException;
import com.auth0.jwt.interfaces.DecodedJWT;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import java.security.SecureRandom;
import java.time.Instant;
import java.util.Base64;
import java.util.Calendar;
import java.util.Date;
import java.util.UUID;

public class RefreshToken {

private static final SecureRandom RANDOM = new SecureRandom();

private static final int RANDOM_SIZE = 128;
private static final String SECRET = "mvpowrngiow34ng0";
private static final Algorithm ALGORITHM = Algorithm.HMAC512(SECRET);

public static String generate(UUID userId) {
byte[] rand = new byte[RANDOM_SIZE];
RANDOM.nextBytes(rand);

Calendar expirationDate = Calendar.getInstance();
expirationDate.setTimeInMillis(System.currentTimeMillis());
expirationDate.add(Calendar.YEAR, 1);

return JWT.create()
.withIssuer("e-edu")
.withSubject("refresh_token")
.withClaim("userId", userId.toString())
.withClaim("rand", Base64.getEncoder().encodeToString(rand))
.withIssuedAt(Date.from(Instant.now()))
.withExpiresAt(expirationDate.getTime())
.sign(ALGORITHM);
}

public static VerificationResult verify(String jwt) {
DecodedJWT decodedJWT;

try {
decodedJWT = JWT.decode(jwt);
} catch (JWTDecodeException ignored) {
return new VerificationResult(VerificationStatus.MALFORMED, null);
}

if (decodedJWT.getExpiresAt().before(Date.from(Instant.now())))
return new VerificationResult(VerificationStatus.EXPIRED, null);

try {
ALGORITHM.verify(decodedJWT);
} catch (SignatureVerificationException ignored) {
return new VerificationResult(VerificationStatus.INVALID, null);
}

UUID userId;
try {
userId = UUID.fromString(decodedJWT.getClaim("userId").asString());
} catch (IllegalArgumentException ignored) {
return new VerificationResult(VerificationStatus.MALFORMED, null);
}

return new VerificationResult(VerificationStatus.VERIFIED, userId);
}

@Data
@RequiredArgsConstructor
public static class VerificationResult {

private final VerificationStatus status;
private final UUID userId;

}

public enum VerificationStatus {
VERIFIED, INVALID, EXPIRED, MALFORMED
}

}
9 changes: 9 additions & 0 deletions src/main/resources/schema.graphqls
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ type Mutation {

authenticate(userAuth: UserAuth): UserAuthResult

jwt(jwtRequest: JwtRequest): JwtResult

}

# ================ Generic ================
Expand Down Expand Up @@ -128,6 +130,13 @@ input JwtRequest

type JwtResult {
token: String # JWT; ONLY stored in the frontend state manager!
status: String
}

enum JwtStatus {
INVALID,
EXPIRED,
VERIFIED
}

# ================ Report MicroService ================
Expand Down

0 comments on commit e25372a

Please sign in to comment.