Skip to content
This repository has been archived by the owner on Sep 15, 2023. It is now read-only.

Commit

Permalink
Merge pull request #36 from UbiqueInnovation/develop
Browse files Browse the repository at this point in the history
Created "/v2/onset" endpoint
  • Loading branch information
Armin-Isenring-Bit authored May 3, 2021
2 parents 04e45e9 + 7328c49 commit 1733b9d
Show file tree
Hide file tree
Showing 9 changed files with 352 additions and 79 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@ public class AuthorizationCodeVerifyResponseDto {

private String accessToken;

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package ch.admin.bag.covidcode.authcodegeneration.api;

public class AuthorizationCodeVerifyResponseDtoWrapper {

private AuthorizationCodeVerifyResponseDto dp3tAccessToken;
private AuthorizationCodeVerifyResponseDto checkInAccessToken;

public AuthorizationCodeVerifyResponseDtoWrapper(AuthorizationCodeVerifyResponseDto dp3tAccessToken, AuthorizationCodeVerifyResponseDto checkInAccessToken) {
this.dp3tAccessToken = dp3tAccessToken;
this.checkInAccessToken = checkInAccessToken;
}

public AuthorizationCodeVerifyResponseDtoWrapper() {}


public AuthorizationCodeVerifyResponseDto getDP3TAccessToken() {
return dp3tAccessToken;
}

public void setDP3TAccessToken(AuthorizationCodeVerifyResponseDto dp3tAccessToken) {
this.dp3tAccessToken = dp3tAccessToken;
}

public AuthorizationCodeVerifyResponseDto getCheckInAccessToken() {
return checkInAccessToken;
}

public void setCheckInAccessToken(AuthorizationCodeVerifyResponseDto checkInAccessToken) {
this.checkInAccessToken = checkInAccessToken;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package ch.admin.bag.covidcode.authcodegeneration.api;

public enum TokenType {

DP3T_TOKEN("dp3t", "exposed"), CHECKIN_USERUPLOAD_TOKEN("checkin", "userupload");

private final String scope;
private final String audience;

TokenType(String audience, String scope) {
this.audience = audience;
this.scope = scope;
}

public String getAudience() {
return audience;
}

public String getScope() {
return scope;
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package ch.admin.bag.covidcode.authcodegeneration.service;

import ch.admin.bag.covidcode.authcodegeneration.api.AuthorizationCodeVerifyResponseDto;
import ch.admin.bag.covidcode.authcodegeneration.api.AuthorizationCodeVerifyResponseDtoWrapper;
import ch.admin.bag.covidcode.authcodegeneration.api.TokenType;
import ch.admin.bag.covidcode.authcodegeneration.domain.AuthorizationCode;
import ch.admin.bag.covidcode.authcodegeneration.domain.AuthorizationCodeRepository;
import lombok.RequiredArgsConstructor;
Expand All @@ -11,7 +13,11 @@

import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;

import static ch.admin.bag.covidcode.authcodegeneration.api.TokenType.CHECKIN_USERUPLOAD_TOKEN;
import static ch.admin.bag.covidcode.authcodegeneration.api.TokenType.DP3T_TOKEN;
import static net.logstash.logback.argument.StructuredArguments.kv;

@Service
Expand All @@ -20,42 +26,91 @@
@RequiredArgsConstructor
public class AuthCodeVerificationService {

private static final String FAKE_STRING = "1";
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("YYYY-MM-dd");
private final AuthorizationCodeRepository authorizationCodeRepository;
private final CustomTokenProvider tokenProvider;
private static final String FAKE_STRING = "1";
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("YYYY-MM-dd");
private final AuthorizationCodeRepository authorizationCodeRepository;
private final CustomTokenProvider tokenProvider;

@Value("${authcodegeneration.service.callCountLimit}")
private int callCountLimit;
@Value("${authcodegeneration.service.callCountLimit}")
private int callCountLimit;

@Transactional
public AuthorizationCodeVerifyResponseDto verify(String code, String fake) {
@Transactional
public AuthorizationCodeVerifyResponseDto verify(String code, String fake) {
final var accessTokens = verify(code, fake, false);
return accessTokens.getDP3TAccessToken();
}

if (FAKE_STRING.equals(fake)) {
log.debug("Fake Call of verification !");
return new AuthorizationCodeVerifyResponseDto(tokenProvider.createToken(AuthorizationCode.createFake().getOnsetDate().format(DATE_FORMATTER), fake));
}

AuthorizationCode existingCode = authorizationCodeRepository.findByCode(code).orElse(null);

if (existingCode == null) {
log.error("No AuthCode found with code '{}'", code);
return null;
} else if (codeValidityHasExpired(existingCode.getExpiryDate())) {
log.error("AuthCode '{}' expired at {}", code, existingCode.getExpiryDate());
return null;
} else if (existingCode.getCallCount() >= this.callCountLimit) {
log.error("AuthCode '{}' reached call limit {}", code, existingCode.getCallCount());
return null;
}
/**
* @param code Authorization code as provided by the health authority
* @param fake String to request fake token
* @param needCheckInToken Needs a second token for purple (checkIn) backend
* @return a wrapper containing two access tokens, which are null if authCode is invalid
*/
@Transactional
public AuthorizationCodeVerifyResponseDtoWrapper verify(
String code, String fake, boolean needCheckInToken) {
final var accessTokens = new AuthorizationCodeVerifyResponseDtoWrapper();
if (FAKE_STRING.equals(fake)) {
final var dp3tToken =
new AuthorizationCodeVerifyResponseDto(
tokenProvider.createToken(
AuthorizationCode.createFake().getOnsetDate().format(DATE_FORMATTER),
FAKE_STRING,
DP3T_TOKEN));
accessTokens.setDP3TAccessToken(dp3tToken);
if (needCheckInToken) {
final var checkInToken =
new AuthorizationCodeVerifyResponseDto(
tokenProvider.createToken(
AuthorizationCode.createFake().getOnsetDate().format(DATE_FORMATTER),
FAKE_STRING,
CHECKIN_USERUPLOAD_TOKEN));
accessTokens.setCheckInAccessToken(checkInToken);
}
return accessTokens;
}

existingCode.incrementCallCount();
log.debug("AuthorizationCode verified: '{}', '{}', '{}', '{}', '{}'", kv("id", existingCode.getId()), kv("callCount", existingCode.getCallCount()), kv("creationDateTime", existingCode.getCreationDateTime().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)), kv("onsetDate",existingCode.getOnsetDate().format(DateTimeFormatter.ISO_LOCAL_DATE)), kv("originalOnsetDate",existingCode.getOriginalOnsetDate().format(DateTimeFormatter.ISO_LOCAL_DATE)));
return new AuthorizationCodeVerifyResponseDto(tokenProvider.createToken(existingCode.getOnsetDate().format(DATE_FORMATTER), fake));
AuthorizationCode existingCode = authorizationCodeRepository.findByCode(code).orElse(null);

if (existingCode == null) {
log.error("No AuthCode found with code '{}'", code);
return accessTokens;
} else if (codeValidityHasExpired(existingCode.getExpiryDate())) {
log.error("AuthCode '{}' expired at {}", code, existingCode.getExpiryDate());
return accessTokens;
} else if (existingCode.getCallCount() >= this.callCountLimit) {
log.error("AuthCode '{}' reached call limit {}", code, existingCode.getCallCount());
return accessTokens;
}

private boolean codeValidityHasExpired(ZonedDateTime expiryDate) {
return expiryDate.isBefore(ZonedDateTime.now());
existingCode.incrementCallCount();
log.debug(
"AuthorizationCode verified: '{}', '{}', '{}', '{}', '{}'",
kv("id", existingCode.getId()),
kv("callCount", existingCode.getCallCount()),
kv(
"creationDateTime",
existingCode.getCreationDateTime().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)),
kv("onsetDate", existingCode.getOnsetDate().format(DateTimeFormatter.ISO_LOCAL_DATE)),
kv(
"originalOnsetDate",
existingCode.getOriginalOnsetDate().format(DateTimeFormatter.ISO_LOCAL_DATE)));
final var swissCovidToken =
new AuthorizationCodeVerifyResponseDto(
tokenProvider.createToken(
existingCode.getOnsetDate().format(DATE_FORMATTER), fake, DP3T_TOKEN));
accessTokens.setDP3TAccessToken(swissCovidToken);
if (needCheckInToken) {
final var checkInToken =
new AuthorizationCodeVerifyResponseDto(
tokenProvider.createToken(
existingCode.getOnsetDate().format(DATE_FORMATTER), fake, CHECKIN_USERUPLOAD_TOKEN));
accessTokens.setCheckInAccessToken(checkInToken);
}
return accessTokens;
}

private boolean codeValidityHasExpired(ZonedDateTime expiryDate) {
return expiryDate.isBefore(ZonedDateTime.now());
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package ch.admin.bag.covidcode.authcodegeneration.service;

import ch.admin.bag.covidcode.authcodegeneration.api.TokenType;
import io.jsonwebtoken.Header;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.io.Decoders;
import lombok.extern.slf4j.Slf4j;
Expand All @@ -17,53 +17,58 @@
import java.util.Date;
import java.util.UUID;

import static ch.admin.bag.covidcode.authcodegeneration.api.TokenType.DP3T_TOKEN;

@Component
@Slf4j
public class CustomTokenProvider {

@Value("${authcodegeneration.jwt.token-validity}")
private long tokenValidity;

@Value("${authcodegeneration.jwt.issuer}")
private String issuer;
@Value("${authcodegeneration.jwt.token-validity}")
private long tokenValidity;

@Value("${authcodegeneration.jwt.privateKey}")
private String privateKey;
@Value("${authcodegeneration.jwt.issuer}")
private String issuer;

private KeyFactory rsa;
@Value("${authcodegeneration.jwt.privateKey}")
private String privateKey;

@PostConstruct
public void init() throws NoSuchAlgorithmException {
rsa = KeyFactory.getInstance("RSA");
}
private KeyFactory rsa;

public String createToken(String onsetDate, String fake) {
final long nowMillis = System.currentTimeMillis();
final Date now = new Date(nowMillis);
@PostConstruct
public void init() throws NoSuchAlgorithmException {
rsa = KeyFactory.getInstance("RSA");
}

final PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(Decoders.BASE64.decode(privateKey));
final Key signingKey;
public String createToken(String onsetDate, String fake) {
return createToken(onsetDate, fake, DP3T_TOKEN);
}

try {
signingKey = rsa.generatePrivate(spec);
} catch (InvalidKeySpecException e) {
log.error("Error during generate private key", e);
throw new IllegalStateException(e);
}
public String createToken(String onsetDate, String fake, TokenType tokenType) {
final long nowMillis = System.currentTimeMillis();
final Date now = new Date(nowMillis);

final JwtBuilder builder = Jwts.builder()
.setId(UUID.randomUUID().toString())
.setIssuer(issuer)
.setIssuedAt(now)
.setNotBefore(now)
.setHeaderParam(Header.TYPE, Header.JWT_TYPE)
.claim("scope", "exposed")
.claim("fake", fake)
.claim("onset", onsetDate)
.signWith(signingKey);
final PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(Decoders.BASE64.decode(privateKey));
final Key signingKey;

builder.setExpiration(new Date(nowMillis + tokenValidity));
return builder.compact();
try {
signingKey = rsa.generatePrivate(spec);
} catch (InvalidKeySpecException e) {
log.error("Error during generate private key", e);
throw new IllegalStateException(e);
}

return Jwts.builder()
.setId(UUID.randomUUID().toString())
.setIssuer(issuer)
.setIssuedAt(now)
.setNotBefore(now)
.setHeaderParam(Header.TYPE, Header.JWT_TYPE)
.claim("aud", tokenType.getAudience())
.claim("scope", tokenType.getScope())
.claim("fake", fake)
.claim("onset", onsetDate)
.signWith(signingKey)
.setExpiration(new Date(nowMillis + tokenValidity))
.compact();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package ch.admin.bag.covidcode.authcodegeneration.web.controller;

import ch.admin.bag.covidcode.authcodegeneration.api.AuthorizationCodeVerificationDto;
import ch.admin.bag.covidcode.authcodegeneration.api.AuthorizationCodeVerifyResponseDto;
import ch.admin.bag.covidcode.authcodegeneration.api.AuthorizationCodeVerifyResponseDtoWrapper;
import ch.admin.bag.covidcode.authcodegeneration.service.AuthCodeVerificationService;
import io.swagger.v3.oas.annotations.Operation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.rest.webmvc.ResourceNotFoundException;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;
import java.time.Duration;
import java.time.Instant;
import java.util.List;

@RestController
@RequestMapping("/v2/onset")
@Slf4j
public class AuthCodeVerificationControllerV2 {

private final AuthCodeVerificationService authCodeVerificationService;
private final Duration requestTime;

public AuthCodeVerificationControllerV2(AuthCodeVerificationService authCodeVerificationService, @Value("${authcodegeneration.service.requestTime}") long requestTime) {
this.authCodeVerificationService = authCodeVerificationService;
this.requestTime = Duration.ofMillis(requestTime);
}

@Operation(summary = "Authorization code verification method")
@PostMapping()
public ResponseEntity<AuthorizationCodeVerifyResponseDtoWrapper> verify(@Valid @RequestBody AuthorizationCodeVerificationDto verificationDto) {
var now = Instant.now().toEpochMilli();
log.debug("Call of Verify with authCode '{}'.", verificationDto.getAuthorizationCode());
final AuthorizationCodeVerifyResponseDtoWrapper accessTokenWrapper = authCodeVerificationService.verify(verificationDto.getAuthorizationCode(), verificationDto.getFake(), true);
normalizeRequestTime(now);
if (accessTokenWrapper.getDP3TAccessToken() == null || accessTokenWrapper.getCheckInAccessToken() == null) {
throw new ResourceNotFoundException(null);
}
return ResponseEntity.ok().body(accessTokenWrapper);
}

private void normalizeRequestTime(long now) {
long after = Instant.now().toEpochMilli();
long duration = after - now;
try {
Thread.sleep(Math.max(requestTime.minusMillis(duration).toMillis(), 0));
} catch (Exception ex) {
log.error("Error during sleep", ex);
}
}
}
Loading

0 comments on commit 1733b9d

Please sign in to comment.