This repository has been archived by the owner on May 22, 2021. It is now read-only.
generated from E-Edu/microservice-template
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #35 from E-Edu/issue/3
Issue/3
- Loading branch information
Showing
7 changed files
with
327 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
67 changes: 66 additions & 1 deletion
67
...ava/de/themorpheus/edu/gateway/graphql/resolver/mutation/user/AuthenticationResolver.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
|
||
} |
87 changes: 87 additions & 0 deletions
87
src/main/java/de/themorpheus/edu/gateway/graphql/resolver/util/HeaderUtil.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
|
||
} | ||
|
||
} |
72 changes: 72 additions & 0 deletions
72
src/main/java/de/themorpheus/edu/gateway/graphql/resolver/util/JsonWebToken.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
|
||
} |
84 changes: 84 additions & 0 deletions
84
src/main/java/de/themorpheus/edu/gateway/graphql/resolver/util/RefreshToken.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters