diff --git a/Dockerfile b/Dockerfile index 189dbc5b8a38d28c4807f7d05aa8b7a7bd39df30..0ec2cacc465133bd619999f4182150bdb5825d04 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,9 +18,7 @@ ENV ACCOUNT_SERVICE_GRPC_PORT=6565 # Install curl for use with Kubernetes readiness probe. RUN apt-get update && apt-get install -y \ curl \ - #Temporay install telnet for troubleshooting purpuses. - telnet #\ -# && rm -rf /var/lib/apt/lists/* + && rm -rf /var/lib/apt/lists/* # Expose Tomcat and gRPC server ports EXPOSE $HTTP_SERVER_PORT diff --git a/Jenkinsfile b/Jenkinsfile index 45889263bd2bd0867a8d071e0fadc88aee65ce79..2a663ddd9f471238515291580d43ef09a79bd9ee 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -117,7 +117,7 @@ pipeline { stage("Approval: Deploy to staging ?") { steps { slackSend channel: SLACK_CHANNEL, message: "$APP_NAME: build #$BUILD_NUMBER ready to deploy to `STAGING`, approval required: $BUILD_URL (24h)" - + timeout(time: 24, unit: 'HOURS') { input 'Deploy to staging ?' } } post { failure { echo 'Deploy aborted for build #...' }} @@ -131,7 +131,7 @@ pipeline { stage("Approval: Deploy to production ?") { steps { slackSend channel: SLACK_CHANNEL, message: "$APP_NAME: build #$BUILD_NUMBER ready to deploy to `PRODUCTION`, approval required: $BUILD_URL (24h)" - + timeout(time: 7, unit: 'DAYS') { input 'Deploy to production ?' } } post { failure { echo 'Deploy aborted for build #...' }} diff --git a/charts/auth-service/templates/deployment.yaml b/charts/auth-service/templates/deployment.yaml index 6328137f7d94fff3f5ec6bab353c0cee6bf26176..0eca9d57c678cd8376f753b716ef64604847dfed 100644 --- a/charts/auth-service/templates/deployment.yaml +++ b/charts/auth-service/templates/deployment.yaml @@ -35,7 +35,7 @@ spec: command: - /bin/sh - -c - - "curl -v --silent http://localhost:$HTTP_SERVER_PORT/actuator/health 2>&1 | grep UP || exit 1" + - "curl --silent http://localhost:$HTTP_SERVER_PORT/actuator/health 2>&1 | grep UP || exit 1" successThreshold: 1 failureThreshold: 10 initialDelaySeconds: 60 diff --git a/releases/dev/auth-service.yaml b/releases/dev/auth-service.yaml index 8f32e335347dd7e088b93785524261b7fb4304b3..0b402ed40e95b63b52097cd203229e0530f73977 100644 --- a/releases/dev/auth-service.yaml +++ b/releases/dev/auth-service.yaml @@ -86,6 +86,8 @@ spec: corsPolicy: allowOrigin: - http://localhost:3000 + - http://localhost + - http://localhost/grpc/ - https://localhost - https://localhost/grpc/ - http://10.191.224.180:3000 @@ -104,4 +106,5 @@ spec: - content-type - x-grpc-web - authorization + - accessToken # To delete after 15 Nov. For reference abotev. maxAge: "600s" diff --git a/src/main/java/biz/nynja/auth/grpc/key/FileManager.java b/src/main/java/biz/nynja/auth/grpc/key/FileManager.java index ef0b7463a205f0670e6a33b86c481b28838ba279..7d1be24dd96ae9489ebeb7e9e3c716fbfdf39c4d 100644 --- a/src/main/java/biz/nynja/auth/grpc/key/FileManager.java +++ b/src/main/java/biz/nynja/auth/grpc/key/FileManager.java @@ -177,7 +177,6 @@ public class FileManager { return jwk; } - private JWKSet loadJWKSetFromFile(File file) throws IOException, ParseException { logger.debug("Reading keys from file: {}", file.getAbsolutePath()); diff --git a/src/main/java/biz/nynja/auth/grpc/services/AuthenticationServiceImpl.java b/src/main/java/biz/nynja/auth/grpc/services/AuthenticationServiceImpl.java index b1332e0cbc1db7187d9c301bb23b7168288bb5d1..f53914cd8f115981bb3db1c6871b5a41f5164e6d 100644 --- a/src/main/java/biz/nynja/auth/grpc/services/AuthenticationServiceImpl.java +++ b/src/main/java/biz/nynja/auth/grpc/services/AuthenticationServiceImpl.java @@ -15,6 +15,7 @@ import org.springframework.beans.factory.annotation.Autowired; import biz.nynja.account.grpc.AuthProviderDetails; import biz.nynja.auth.grpc.integrations.AccountServiceCommunicator; +import biz.nynja.auth.grpc.services.interceptors.AuthenticationServiceConstants; import biz.nynja.auth.grpc.social.AccessTokenResponseProvider; import biz.nynja.auth.grpc.token.GrpcObjectBuilder; import biz.nynja.auth.grpc.token.TokenConfig; @@ -26,6 +27,7 @@ import biz.nynja.auth.grpc.token.refresh.RefreshTokenService; import biz.nynja.auth.grpc.token.verify.VerifyCodeGenerator; import biz.nynja.auth.grpc.token.verify.VerifyTokenService; import biz.nynja.authentication.grpc.AuthenticationServiceGrpc; +import biz.nynja.authentication.grpc.EmptyRequest; import biz.nynja.authentication.grpc.ErrorResponse; import biz.nynja.authentication.grpc.ErrorResponse.Cause; import biz.nynja.authentication.grpc.ExchangeRefreshTokenRequest; @@ -69,8 +71,9 @@ public class AuthenticationServiceImpl extends AuthenticationServiceGrpc.Authent private RefreshTokenService refreshTokenService; @Autowired - public AuthenticationServiceImpl(AuthTokenService authTokenService, Validator validator, VerifyTokenService verifyTokenService, - VerifyCodeGenerator verifyCodeGenerator, AccountServiceCommunicator accountService, AccessTokenResponseProvider accessTokenResponseProvider, + public AuthenticationServiceImpl(AuthTokenService authTokenService, Validator validator, + VerifyTokenService verifyTokenService, VerifyCodeGenerator verifyCodeGenerator, + AccountServiceCommunicator accountService, AccessTokenResponseProvider accessTokenResponseProvider, AccessTokenService accessTokenService, TokenConfig tokenConfig, RefreshTokenService refreshTokenService) { this.authTokenService = authTokenService; this.validator = validator; @@ -175,8 +178,8 @@ public class AuthenticationServiceImpl extends AuthenticationServiceGrpc.Authent responseObserver.onNext(GenerateAccessTokenResponse.newBuilder().setError(ErrorResponse.newBuilder() .setMessage(e.getMessage()).setCause(Cause.valueOf(e.getCause().getMessage()))).build()); } else { - responseObserver.onNext(GenerateAccessTokenResponse.newBuilder().setError(ErrorResponse.newBuilder() - .setMessage(e.getMessage())).build()); + responseObserver.onNext(GenerateAccessTokenResponse.newBuilder() + .setError(ErrorResponse.newBuilder().setMessage(e.getMessage())).build()); } responseObserver.onCompleted(); return; @@ -197,17 +200,23 @@ public class AuthenticationServiceImpl extends AuthenticationServiceGrpc.Authent logger.debug("Exchange refresh token for new access token request recieved: {}", request.getRefreshToken()); Optional decodedToken = refreshTokenService.decodeRefreshToken(request.getRefreshToken()); - if (decodedToken.isPresent() && refreshTokenService.validateRefreshToken(decodedToken.get())) { - String token = accessTokenService.createAccessToken(decodedToken.get()); - - responseObserver - .onNext(GenerateTokenResponse.newBuilder() - .setTokenResponseDetails(TokenResponseDetails.newBuilder().setToken(token) - .setExp(tokenConfig.getAccessExpiresIn()) - .setResponseTokenType(ResponseTokenType.BEARER).build()) - .build()); - responseObserver.onCompleted(); - return; + Optional> isValid = refreshTokenService.validateRefreshToken(decodedToken.get()); + if (decodedToken.isPresent()) { + if (!isValid.isPresent()) { + String token = accessTokenService.createAccessToken(decodedToken.get()); + + responseObserver.onNext(GenerateTokenResponse.newBuilder() + .setTokenResponseDetails(TokenResponseDetails.newBuilder().setToken(token) + .setExp(tokenConfig.getAccessExpiresIn()).setResponseTokenType(ResponseTokenType.BEARER) + .build()) + .build()); + responseObserver.onCompleted(); + return; + } else { + responseObserver.onNext(GenerateTokenResponse.newBuilder().setError(ErrorResponse.newBuilder() + .setMessage(isValid.get().getLeft()).setCause(isValid.get().getRight())).build()); + responseObserver.onCompleted(); + } } else { responseObserver.onNext(GenerateTokenResponse.newBuilder() .setError(ErrorResponse.newBuilder().setCause(Cause.REFRESH_TOKEN_INVALID)).build()); @@ -228,7 +237,7 @@ public class AuthenticationServiceImpl extends AuthenticationServiceGrpc.Authent accessTokenService.checkFailedDeviceAttemptsCounterAndTimeout(request.getDeviceId()); } logger.debug("Couldn't generate google access token."); - if (e instanceof InternalError ) { + if (e instanceof InternalError) { throw new InternalError(e.getMessage(), new InternalError(e.getCause().getMessage())); } else { throw new InternalError("Couldn't generate google access token."); @@ -286,4 +295,26 @@ public class AuthenticationServiceImpl extends AuthenticationServiceGrpc.Authent } } + @Override + public void generateAdminAccessToken(EmptyRequest request, StreamObserver responseObserver) { + + String accessToken = AuthenticationServiceConstants.ACCESS_TOKEN_CTX.get(); + + Optional token = accessTokenService.retrieveAdminAccessToken(accessToken); + if (token.isPresent()) { + TokenResponseDetails tokenResponseDetails = TokenResponseDetails.newBuilder().setToken(token.get()) + .setExp(tokenConfig.getAdminAccessExpiresIn()).setResponseTokenType(ResponseTokenType.BEARER) + .build(); + responseObserver + .onNext(GenerateTokenResponse.newBuilder().setTokenResponseDetails(tokenResponseDetails).build()); + responseObserver.onCompleted(); + return; + } else { + responseObserver.onNext(GenerateTokenResponse.newBuilder() + .setError(ErrorResponse.newBuilder().setCause(Cause.ACCESS_TOKEN_INVALID_ROLE)).build()); + responseObserver.onCompleted(); + return; + } + } + } diff --git a/src/main/java/biz/nynja/auth/grpc/services/interceptors/AuthenticationServiceConstants.java b/src/main/java/biz/nynja/auth/grpc/services/interceptors/AuthenticationServiceConstants.java new file mode 100644 index 0000000000000000000000000000000000000000..80fa178c9ea6876684705a18499c23754e79bb95 --- /dev/null +++ b/src/main/java/biz/nynja/auth/grpc/services/interceptors/AuthenticationServiceConstants.java @@ -0,0 +1,13 @@ +package biz.nynja.auth.grpc.services.interceptors; + +import static io.grpc.Metadata.ASCII_STRING_MARSHALLER; + +import io.grpc.Context; +import io.grpc.Metadata; + +public class AuthenticationServiceConstants { + + public static final Metadata.Key ACCESS_TOKEN_METADATA = Metadata.Key.of("accessToken", ASCII_STRING_MARSHALLER); + public static final Context.Key ACCESS_TOKEN_CTX = Context.key("accessToken"); + +} diff --git a/src/main/java/biz/nynja/auth/grpc/services/interceptors/AuthenticationServiceInterceptor.java b/src/main/java/biz/nynja/auth/grpc/services/interceptors/AuthenticationServiceInterceptor.java new file mode 100644 index 0000000000000000000000000000000000000000..746a81a8d831b618179733cded1d8ab50b8a5bb3 --- /dev/null +++ b/src/main/java/biz/nynja/auth/grpc/services/interceptors/AuthenticationServiceInterceptor.java @@ -0,0 +1,39 @@ +package biz.nynja.auth.grpc.services.interceptors; + +import org.lognet.springboot.grpc.GRpcGlobalInterceptor; + +import io.grpc.Context; +import io.grpc.Contexts; +import io.grpc.Metadata; +import io.grpc.ServerCall; +import io.grpc.ServerCallHandler; +import io.grpc.ServerInterceptor; + +@GRpcGlobalInterceptor +public class AuthenticationServiceInterceptor implements ServerInterceptor { + // This should be returned if there is an error in the request to stop from going in + // private static final ServerCall.Listener NOOP_LISTENER = new ServerCall.Listener() { + // }; + + @Override + public ServerCall.Listener interceptCall(ServerCall serverCall, Metadata metadata, + ServerCallHandler serverCallHandler) { + String accessToken = metadata.get(AuthenticationServiceConstants.ACCESS_TOKEN_METADATA); + Context ctx = Context.current().withValue(AuthenticationServiceConstants.ACCESS_TOKEN_CTX, accessToken); + return Contexts.interceptCall(ctx, serverCall, metadata, serverCallHandler); + } + + /* + * We could use a method like this to read some variables from JWT token if we need to like roles and so on public + * void jwtCheckMethod() { + * + * // if (jwt == null) { // + * serverCall.close(Status.UNAUTHENTICATED.withDescription("JWT Token is missing from Metadata"), metadata); // + * return NOOP_LISTENER; // } // // Context ctx; // try { // Map verified = verifier.verify(jwt); // + * ctx = Context.current().withValue(Constant.USER_ID_CTX_KEY, verified.getOrDefault("sub", // + * "anonymous").toString()) // .withValue(Constant.JWT_CTX_KEY, jwt); // } catch (Exception e) { // + * System.out.println("Verification failed - Unauthenticated!"); // + * serverCall.close(Status.UNAUTHENTICATED.withDescription(e.getMessage()).withCause(e), metadata); // return + * NOOP_LISTENER; // } } + */ +} diff --git a/src/main/java/biz/nynja/auth/grpc/social/AccessTokenResponseProvider.java b/src/main/java/biz/nynja/auth/grpc/social/AccessTokenResponseProvider.java index 3d6baa0df2cf33841f4fccc3057ca8855edf069c..41ff073f6c044f88ac13cb390cf0ead82f68671c 100644 --- a/src/main/java/biz/nynja/auth/grpc/social/AccessTokenResponseProvider.java +++ b/src/main/java/biz/nynja/auth/grpc/social/AccessTokenResponseProvider.java @@ -85,9 +85,8 @@ public class AccessTokenResponseProvider { if (isSocialProvider(request.getSidType())) { socialAccessTokenRepository.save(buildSocialAccessToken(detailsBean, accountProperties.getAccountId())); } - String accessToken = accessTokenService.createAccessToken(request.getInstanceId(), - request.getAppClass(), request.getOrgId(), accountProperties.getAccountId(), - accountProperties.getRoles()); + String accessToken = accessTokenService.createAccessToken(request.getInstanceId(), request.getAppClass(), + request.getOrgId(), accountProperties.getAccountId(), accountProperties.getRoles(), false); response.setToken(accessToken); response.setToken_type(ResponseTokenType.BEARER); response.setExpires_in(access_expiration); diff --git a/src/main/java/biz/nynja/auth/grpc/token/TokenConfig.java b/src/main/java/biz/nynja/auth/grpc/token/TokenConfig.java index 78367efe451235c8242c0ff9150a5ad19bb09b60..0cf967525adfe173f7dc960e2bf2fbcdd1565bfa 100644 --- a/src/main/java/biz/nynja/auth/grpc/token/TokenConfig.java +++ b/src/main/java/biz/nynja/auth/grpc/token/TokenConfig.java @@ -1,3 +1,6 @@ +/** + * Copyright (C) 2018 Nynja Inc. All rights reserved. + */ package biz.nynja.auth.grpc.token; import org.springframework.beans.factory.annotation.Autowired; @@ -29,6 +32,8 @@ public class TokenConfig { private final String accountServiceAddress; private final int accountServicePort; + private final int adminAccessExpiresIn; + private final String appIssuer; private final String appAud; private final String appSub; @@ -57,6 +62,8 @@ public class TokenConfig { .parseInt(env.getRequiredProperty("token.access.failed_device_attempts_timeslot")); this.accessScope = env.getRequiredProperty("token.access.scope"); + this.adminAccessExpiresIn = parseProperty(env, "token.admin.expires_in"); + this.maxFailedAttempts = parseProperty(env, "token.access.max_failed_attempts"); this.failedAttemptsTimeslot = Integer .parseInt(env.getRequiredProperty("token.access.failed_attempts_timeslot")); @@ -174,4 +181,9 @@ public class TokenConfig { public String getRefreshSecretKey() { return refreshSecretKey; } + + public int getAdminAccessExpiresIn() { + return adminAccessExpiresIn; + } + } diff --git a/src/main/java/biz/nynja/auth/grpc/token/TokenResponse.java b/src/main/java/biz/nynja/auth/grpc/token/TokenResponse.java index 0bbef973109f6adc47a8aecf58f3c3e7450638de..b8d8bcbefd2b8bb628ac0e52a82379bae7e563cf 100644 --- a/src/main/java/biz/nynja/auth/grpc/token/TokenResponse.java +++ b/src/main/java/biz/nynja/auth/grpc/token/TokenResponse.java @@ -27,6 +27,7 @@ public class TokenResponse { private String accountId; + private String message; public TokenResponse() { } @@ -79,4 +80,12 @@ public class TokenResponse { this.accountId = accountId; } + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + } diff --git a/src/main/java/biz/nynja/auth/grpc/token/access/AccessTokenController.java b/src/main/java/biz/nynja/auth/grpc/token/access/AccessTokenController.java index 27926dd4d36fa405db611cd47128486dd43595f2..17f3d82248fb91b9f6d780aa2a08c00441e99b0d 100644 --- a/src/main/java/biz/nynja/auth/grpc/token/access/AccessTokenController.java +++ b/src/main/java/biz/nynja/auth/grpc/token/access/AccessTokenController.java @@ -4,21 +4,23 @@ package biz.nynja.auth.grpc.token.access; import java.io.IOException; +import java.util.Optional; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; import biz.nynja.account.grpc.ErrorResponse.Cause; import biz.nynja.auth.grpc.social.AccessTokenResponseProvider; +import biz.nynja.auth.grpc.token.TokenConfig; import biz.nynja.auth.grpc.token.TokenResponse; import biz.nynja.auth.grpc.token.access.models.AccessTokenRequestBean; +import biz.nynja.authentication.grpc.ResponseTokenType; /** * @author Angel.Botev @@ -31,14 +33,17 @@ public class AccessTokenController { private final AccessTokenResponseProvider accessTokenResponseProvider; + private final TokenConfig tokenConfig; + + private final AccessTokenService accessTokenService; + public AccessTokenController(AccessTokenService accessTokenService, - AccessTokenResponseProvider accessTokenResponseProvider) { + AccessTokenResponseProvider accessTokenResponseProvider, TokenConfig tokenConfig) { this.accessTokenResponseProvider = accessTokenResponseProvider; + this.tokenConfig = tokenConfig; + this.accessTokenService = accessTokenService; } - @Value("${token.access.expires_in}") - private Integer access_expiration; - @RequestMapping(path = "/tokens/access", method = RequestMethod.POST) public ResponseEntity generateAccessToken(@RequestBody AccessTokenRequestBean request) throws InternalError { @@ -50,4 +55,22 @@ public class AccessTokenController { throw new InternalError(e.getMessage(), new InternalError(Cause.INTERNAL_SERVER_ERROR.toString())); } } + + @RequestMapping(path = "/tokens/access/admin", method = RequestMethod.POST) + public ResponseEntity generateAdminAccessToken(@RequestHeader String token) throws InternalError { + + logger.info("Exchange access token for new admin access token request recieved."); + logger.debug("Exchange access token for new admin access token request recieved: {}", token); + + Optional newAdminAccessToken = accessTokenService.retrieveAdminAccessToken(token); + if (!newAdminAccessToken.isPresent()) { + throw new IllegalArgumentException("The account associated with this account_id doesn't have rights!"); + } + TokenResponse response = new TokenResponse(); + response.setToken(newAdminAccessToken.get()); + response.setExpires_in(tokenConfig.getAdminAccessExpiresIn()); + response.setToken_type(ResponseTokenType.BEARER); + + return ResponseEntity.ok(response); + } } diff --git a/src/main/java/biz/nynja/auth/grpc/token/access/AccessTokenService.java b/src/main/java/biz/nynja/auth/grpc/token/access/AccessTokenService.java index 17c6cd316eafe14dd8e62a82c521ab2e920b9c9f..b4dc9b95bab55a42c6990fd3aac0e93968f95661 100644 --- a/src/main/java/biz/nynja/auth/grpc/token/access/AccessTokenService.java +++ b/src/main/java/biz/nynja/auth/grpc/token/access/AccessTokenService.java @@ -11,8 +11,10 @@ import java.text.ParseException; import java.util.Base64; import java.util.Calendar; import java.util.Date; +import java.util.Optional; import java.util.Set; import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; import javax.crypto.BadPaddingException; import javax.crypto.Cipher; @@ -30,12 +32,16 @@ import org.springframework.util.StringUtils; import com.auth0.jwt.JWT; import com.auth0.jwt.algorithms.Algorithm; +import com.auth0.jwt.interfaces.DecodedJWT; import com.nimbusds.jose.JOSEException; import com.nimbusds.jose.jwk.ECKey; import com.nimbusds.jose.jwk.JWK; +import biz.nynja.account.grpc.AccountResponse; import biz.nynja.account.grpc.AuthProviderDetails; import biz.nynja.account.grpc.AuthenticationType; +import biz.nynja.account.grpc.Role; +import biz.nynja.auth.grpc.integrations.AccountServiceCommunicator; import biz.nynja.auth.grpc.key.FileManager; import biz.nynja.auth.grpc.security.bruteforce.models.CountDeviceFailedAttempts; import biz.nynja.auth.grpc.security.bruteforce.models.CountPhoneFailedAttempts; @@ -61,16 +67,19 @@ public class AccessTokenService { private Encryptor encryptor; + private AccountServiceCommunicator accountServiceCommunicator; + @Autowired public AccessTokenService(TokenConfig tokenConfig, CountPhoneFailedAttempsRepository countPhoneFailedAttempsRepository, CountDeviceFailedAttempsRepository countDeviceFailedAttempsRepository, FileManager keyFileManager, - Encryptor encryptor) { + Encryptor encryptor, AccountServiceCommunicator accountServiceCommunicator) { this.tokenConfig = tokenConfig; this.countPhoneFailedAttempsRepository = countPhoneFailedAttempsRepository; this.countDeviceFailedAttempsRepository = countDeviceFailedAttempsRepository; this.keyFileManager = keyFileManager; this.encryptor = encryptor; + this.accountServiceCommunicator = accountServiceCommunicator; } /** @@ -142,11 +151,9 @@ public class AccessTokenService { * @throws InternalError */ public String createAccessToken(String instanceId, String appClass, String orgId, String accountId, - Set roles) - throws InternalError { + Set roles, boolean isAdmin) throws InternalError { if (StringUtils.isEmpty(orgId)) { orgId = tokenConfig.getAccessDefaultOrgId(); - } ECKey ecKey = null; @@ -164,7 +171,11 @@ public class AccessTokenService { Calendar cal = Calendar.getInstance(); Date iat = cal.getTime(); - cal.add(Calendar.SECOND, tokenConfig.getAccessExpiresIn()); + if (isAdmin) { + cal.add(Calendar.SECOND, tokenConfig.getAdminAccessExpiresIn()); + } else { + cal.add(Calendar.SECOND, tokenConfig.getAccessExpiresIn()); + } Date exp = cal.getTime(); String aud = new StringBuilder(Base64.getEncoder().encodeToString(instanceId.getBytes())).append(":") @@ -268,7 +279,8 @@ public class AccessTokenService { if ((currentCount >= tokenConfig.getMaxPhoneFailedAttempts()) && (elapsedTime < timeslotAllowed)) { logger.debug("Failed phone attempts counter is over the limit for provider " + sid); - throw new InternalError("Error: Failed phone attempts counter is over the limit for provider " + sid, + throw new InternalError( + "Error: Failed phone attempts counter is over the limit for provider " + sid, new InternalError(Cause.MAX_PHONE_FAILED_ATTEMPTS_REACHED.toString())); } } @@ -325,7 +337,8 @@ public class AccessTokenService { if ((currentCount >= tokenConfig.getMaxDeviceFailedAttempts()) && (elapsedTime < timeslotAllowed)) { logger.debug("Failed device attempts counter is over the limit for device " + deviceId); - throw new InternalError("Error: Failed device attempts counter is over the limit for device " + deviceId, + throw new InternalError( + "Error: Failed device attempts counter is over the limit for device " + deviceId, new InternalError(Cause.MAX_DEVICE_FAILED_ATTEMPTS_REACHED.toString())); } } @@ -366,4 +379,34 @@ public class AccessTokenService { logger.debug("CountDeviceFailedAttempts \"{}\" saved into the DB", savedFailedCount.toString()); } + /** + * Reissues the original token with updated roles field + * + * @param oldToken + * @return + */ + public Optional retrieveAdminAccessToken(String oldToken) { + DecodedJWT decodedToken = JWT.decode(oldToken); + String accountId = new String(Base64.getDecoder().decode(decodedToken.getSubject())); + + AccountResponse accountResponse = accountServiceCommunicator.getAccountById(accountId); + if (accountResponse.getAccountDetails() == null || accountResponse.getAccountDetails().getRolesList().isEmpty() + || (accountResponse.getAccountDetails().getRolesList().size() == 1 + && accountResponse.getAccountDetails().getRolesList().contains(Role.USER))) { + return Optional.empty(); + } + + String[] audienceItems = decodedToken.getAudience().get(0).split(":"); + + // Base64 encoded instanceId : appClass : orgId + String instanceId = new String(Base64.getDecoder().decode(audienceItems[0])); + String appClass = audienceItems[1]; + String orgId = audienceItems[2]; + + String accessToken = createAccessToken(instanceId, appClass, orgId, accountId, accountResponse + .getAccountDetails().getRolesList().stream().map(n -> n.toString()).collect(Collectors.toSet()), true); + + return Optional.of(accessToken); + } + } diff --git a/src/main/java/biz/nynja/auth/grpc/token/refresh/RefreshTokenController.java b/src/main/java/biz/nynja/auth/grpc/token/refresh/RefreshTokenController.java index d4512195a408b98e7568aa6fd51e2bd5ff475654..d5bc98b7b7d5892b1203d667250f2d6d072aaa9a 100644 --- a/src/main/java/biz/nynja/auth/grpc/token/refresh/RefreshTokenController.java +++ b/src/main/java/biz/nynja/auth/grpc/token/refresh/RefreshTokenController.java @@ -7,6 +7,7 @@ import static org.springframework.web.bind.annotation.RequestMethod.GET; import java.util.Optional; +import org.apache.commons.lang3.tuple.ImmutablePair; import org.json.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -20,6 +21,7 @@ import biz.nynja.auth.grpc.token.TokenConfig; import biz.nynja.auth.grpc.token.TokenResponse; import biz.nynja.auth.grpc.token.access.AccessTokenService; import biz.nynja.authentication.grpc.ResponseTokenType; +import biz.nynja.authentication.grpc.ErrorResponse.Cause; /** * @author Angel.Botev @@ -51,7 +53,11 @@ public class RefreshTokenController { logger.debug("Exchange refresh token for new access token request recieved: {}", refreshToken); Optional decodedToken = refreshTokenService.decodeRefreshToken(refreshToken); - if (decodedToken.isPresent() && refreshTokenService.validateRefreshToken(decodedToken.get())) { + if (!decodedToken.isPresent()) { + throw new IllegalArgumentException("Refresh token is not valid!"); + } + Optional> isValid = refreshTokenService.validateRefreshToken(decodedToken.get()); + if (!isValid.isPresent()) { String token = accessTokenService.createAccessToken(decodedToken.get()); TokenResponse tokenResponse = new TokenResponse(); @@ -60,7 +66,9 @@ public class RefreshTokenController { tokenResponse.setExpires_in(tokenConfig.getAccessExpiresIn()); return ResponseEntity.ok(tokenResponse); } else { - throw new IllegalArgumentException("Refresh token is not valid!"); + TokenResponse tokenResponse = new TokenResponse(); + tokenResponse.setMessage(isValid.get().getLeft()); + return ResponseEntity.ok(tokenResponse); } } } diff --git a/src/main/java/biz/nynja/auth/grpc/token/refresh/RefreshTokenService.java b/src/main/java/biz/nynja/auth/grpc/token/refresh/RefreshTokenService.java index 2347cc88184d2b53e505ef6d9d110e78a1abd8d9..d16ed739dcec5d35beef7213104717302a474449 100644 --- a/src/main/java/biz/nynja/auth/grpc/token/refresh/RefreshTokenService.java +++ b/src/main/java/biz/nynja/auth/grpc/token/refresh/RefreshTokenService.java @@ -20,13 +20,16 @@ import javax.crypto.NoSuchPaddingException; import javax.crypto.spec.GCMParameterSpec; import javax.crypto.spec.SecretKeySpec; +import org.apache.commons.lang3.tuple.ImmutablePair; import org.json.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import biz.nynja.account.grpc.AccessStatus; import biz.nynja.account.grpc.AccountResponse; +import biz.nynja.authentication.grpc.ErrorResponse.Cause; import biz.nynja.auth.grpc.integrations.AccountServiceCommunicator; import biz.nynja.auth.grpc.token.Encryptor; import biz.nynja.auth.grpc.token.TokenConfig; @@ -111,9 +114,7 @@ public class RefreshTokenService { byte[] decryptedData = cipher.doFinal(Base64.getDecoder().decode(encodedData[0])); decryptedToken = new String(decryptedData); - } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException - | InvalidAlgorithmParameterException | UnsupportedEncodingException | IllegalBlockSizeException - | BadPaddingException e) { + } catch (Exception e) { logger.error("Error when decoding refresh token {}.", e.getMessage()); logger.debug("Error when decoding refresh token {}.", e.getCause()); return Optional.empty(); @@ -122,16 +123,18 @@ public class RefreshTokenService { return Optional.of(new JSONObject(decryptedToken)); } - public boolean validateRefreshToken(JSONObject tokenDetails) { + public Optional> validateRefreshToken(JSONObject tokenDetails) { logger.debug("Validating refreshToken details: {}", tokenDetails); String accountId = new String(Base64.getDecoder().decode(tokenDetails.getString("sub"))); AccountResponse accountResponse = accountServiceCommunicator.getAccountById(accountId); // TODO Account status is not clear enough. Account should be validated because it could be disabled or deleted. - if (accountResponse.getAccountDetails().getAccessStatus().equals("DISABLED") - || accountResponse.getAccountDetails().getAccessStatus().equals("SUSPENDED")) { + if (accountResponse.getAccountDetails().getAccessStatus().equals(AccessStatus.DISABLED)) { logger.debug("Account by id: {} is disabled", accountId); - return false; + return Optional.of(new ImmutablePair("Account is disabled!", Cause.ACCESS_DISABLED)); + } else if (accountResponse.getAccountDetails().getAccessStatus().equals(AccessStatus.SUSPENDED)) { + logger.debug("Account by id: {} is suspended", accountId); + return Optional.of(new ImmutablePair("Account is suspended!", Cause.ACCESS_SUSPENDED)); } if (tokenDetails.getLong("exp") != 0) { @@ -139,9 +142,9 @@ public class RefreshTokenService { Date refreshTokenExpiration = new Date(tokenDetails.getLong("exp")); if (currentTime.after(refreshTokenExpiration)) { logger.debug("Refresh token: {} is expired!", tokenDetails.toString()); - return false; + return Optional.of(new ImmutablePair("Refresh token has expired!", Cause.EXPIRED_REFRESH_TOKEN)); } } - return true; + return Optional.empty(); } } diff --git a/src/main/java/biz/nynja/auth/grpc/token/verify/VerifyTokenController.java b/src/main/java/biz/nynja/auth/grpc/token/verify/VerifyTokenController.java index 88d71e7bd24b5f229048771517130597996902f9..617684379b819a2e0847f8c813850d57e15c18f2 100644 --- a/src/main/java/biz/nynja/auth/grpc/token/verify/VerifyTokenController.java +++ b/src/main/java/biz/nynja/auth/grpc/token/verify/VerifyTokenController.java @@ -70,7 +70,7 @@ public class VerifyTokenController { throw new InternalError("Verify token could not be issued."); } - if (sendVia == null || sendVia.getNumber() == 0) { + if ((sendVia == null || sendVia.getNumber() == 0) && sidType.getNumber() == SidType.PHONE_VALUE) { throw new IllegalArgumentException("send_via parameter should be one of SMS or CALL"); } diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 403d8046aea41f6ae9a72db7eb5055aca8fc7912..7dfa9720147ccd8ba8a41d0693044e0d7473a901 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -33,6 +33,8 @@ token: failed_device_attempts_timeslot: 1440 max_failed_attempts: 3 failed_attempts_timeslot: 1440 + admin: + expires_in: 28800 #8 hours app: issuer: "https://auth.nynja.biz/" aud: Nynja @@ -70,7 +72,7 @@ amazon: host: email-smtp.us-west-2.amazonaws.com #Amazon SES SMTP host name. For more information: https://docs.aws.amazon.com/ses/latest/DeveloperGuide/regions.html#region-endpoints port: 587 #The port you will connect to on the Amazon SES SMTP endpoint. smtp_username: - smtp_password: + smtp_password: from: noreply@nynja.net template: diff --git a/src/main/resources/application-production.yml b/src/main/resources/application-production.yml index bc72fccc5def01c0788445060ec2ad440ce04467..6c43fcb5ea7dc6fa139bd574f7dab5666091659f 100644 --- a/src/main/resources/application-production.yml +++ b/src/main/resources/application-production.yml @@ -41,6 +41,8 @@ token: # To be removed in the future. max_failed_attempts: 1000 failed_attempts_timeslot: 1440 + admin: + expires_in: 28800 #8 hours app: issuer: "https://auth.nynja.biz/" aud: Nynja diff --git a/src/test/java/biz/nynja/auth/grpc/services/AuthenticationServiceImplTest.java b/src/test/java/biz/nynja/auth/grpc/services/AuthenticationServiceImplTest.java index fb10daa6c359e0f00fc11895c157f3440d4e722e..7efa008bd31a38ed28c251e3505cc20091727bd4 100644 --- a/src/test/java/biz/nynja/auth/grpc/services/AuthenticationServiceImplTest.java +++ b/src/test/java/biz/nynja/auth/grpc/services/AuthenticationServiceImplTest.java @@ -62,6 +62,8 @@ public class AuthenticationServiceImplTest extends GrpcServerTestBase { private final static String PHONE = "BG:+359885555555"; private final static String SID = "BG:+359885555555"; + private final static String EMAIL = "test@mytestemail.com"; + private final static String EMAIL_INVALID = "invalidemail@"; private final static String VERIFY_CODE = "111111"; @MockBean @@ -117,6 +119,20 @@ public class AuthenticationServiceImplTest extends GrpcServerTestBase { reply.getError().getCause().equals(Cause.SID_INVALID)); } + @Test + public void testGenerateVerifyTokenSidEmailInvalid() { + final GenerateVerifyTokenRequest request = GenerateVerifyTokenRequest.newBuilder().setSid(EMAIL_INVALID) + .setSidType(SidType.EMAIL).build(); + + final AuthenticationServiceGrpc.AuthenticationServiceBlockingStub authenticationServiceBlockingStub = AuthenticationServiceGrpc + .newBlockingStub(Optional.ofNullable(channel).orElse(inProcChannel)); + + final GenerateTokenResponse reply = authenticationServiceBlockingStub.generateVerifyToken(request); + assertNotNull("Reply should not be null", reply); + assertTrue(String.format("Reply should contain cause '%s'", Cause.EMAIL_INVALID), + reply.getError().getCause().equals(Cause.EMAIL_INVALID)); + } + @Test public void testGenerateVerifyTokenNullInternalServerError() { final GenerateVerifyTokenRequest request = GenerateVerifyTokenRequest.newBuilder().setSid(SID) @@ -152,6 +168,24 @@ public class AuthenticationServiceImplTest extends GrpcServerTestBase { reply.getTokenResponseDetails().getResponseTokenType().equals(ResponseTokenType.CODE)); } + @Test + public void testGenerateVerifyTokenEmailOK() { + final GenerateVerifyTokenRequest request = GenerateVerifyTokenRequest.newBuilder().setSid(EMAIL) + .setSidType(SidType.EMAIL).setSendVia(SendMethod.IDLE).build(); + + given(verifyCodeGenerator.generateVerifyCode(6)).willReturn(VERIFY_CODE); + given(verifyTokenService.generateVerifyToken(EMAIL, SidType.EMAIL, VERIFY_CODE)).willReturn("VerifyToken"); + given(verifyCodeGenerator.sendVerifyCodeViaEmail(EMAIL, VERIFY_CODE)).willReturn(true); + + final AuthenticationServiceGrpc.AuthenticationServiceBlockingStub authenticationServiceBlockingStub = AuthenticationServiceGrpc + .newBlockingStub(Optional.ofNullable(channel).orElse(inProcChannel)); + + final GenerateTokenResponse reply = authenticationServiceBlockingStub.generateVerifyToken(request); + assertNotNull("Reply should not be null", reply); + assertTrue(String.format("Reply should contain token type '%s'", ResponseTokenType.CODE), + reply.getTokenResponseDetails().getResponseTokenType().equals(ResponseTokenType.CODE)); + } + @Test public void testGenerateVerifyTokenVerifyCodeNotSent() { final GenerateVerifyTokenRequest request = GenerateVerifyTokenRequest.newBuilder().setSid(SID) @@ -205,6 +239,23 @@ public class AuthenticationServiceImplTest extends GrpcServerTestBase { assertNotNull(accessTokenResponse.getAccessTokenResponseDetails().getTokenResponseDetails().getToken()); } + public void testGenerateVerifyTokenVerifyCodeNotSentEmail() { + final GenerateVerifyTokenRequest request = GenerateVerifyTokenRequest.newBuilder().setSid(EMAIL) + .setSidType(SidType.EMAIL).build(); + + given(verifyCodeGenerator.generateVerifyCode(6)).willReturn(VERIFY_CODE); + given(verifyTokenService.generateVerifyToken(EMAIL, SidType.EMAIL, VERIFY_CODE)).willReturn("VerifyToken"); + given(verifyCodeGenerator.sendVerifyCodeViaEmail(EMAIL, VERIFY_CODE)).willReturn(false); + + final AuthenticationServiceGrpc.AuthenticationServiceBlockingStub authenticationServiceBlockingStub = AuthenticationServiceGrpc + .newBlockingStub(Optional.ofNullable(channel).orElse(inProcChannel)); + + final GenerateTokenResponse reply = authenticationServiceBlockingStub.generateVerifyToken(request); + assertNotNull("Reply should not be null", reply); + assertTrue(String.format("Reply should contain cause '%s'", Cause.INTERNAL_SERVER_ERROR), + reply.getError().getCause().equals(Cause.INTERNAL_SERVER_ERROR)); + } + @Test public void testAccessTokenGenerationWrongCode() { Mockito.when(accessTokenService.retrieveAuthProviderDetails(Mockito.anyString(), Mockito.anyString())) diff --git a/src/test/java/biz/nynja/auth/grpc/services/VerifyTokenControllerTest.java b/src/test/java/biz/nynja/auth/grpc/services/VerifyTokenControllerTest.java index 1a028435acac7ed342d6e40a246c0b4e12cd5977..df28ce895f6e29d4bd13d6c403045bdbccd0c0b7 100644 --- a/src/test/java/biz/nynja/auth/grpc/services/VerifyTokenControllerTest.java +++ b/src/test/java/biz/nynja/auth/grpc/services/VerifyTokenControllerTest.java @@ -42,6 +42,8 @@ public class VerifyTokenControllerTest { private final static String PHONE = "BG:+359885555555"; private final static String SID = "BG:+359885555555"; + public static final String EMAIL = "test@mytestemail.com"; + private final static String EMAIL_INVALID = "invalidemail@"; private final static SidType SID_TYPE_PHONE = SidType.PHONE; private final static String SID_INVALID = "incorectSid"; private final static SendMethod SEND_VIA = SendMethod.SMS; @@ -74,6 +76,18 @@ public class VerifyTokenControllerTest { .andExpect(status().isOk()).andExpect(jsonPath("$.token_type", is("CODE"))); } + @Test + public void testGenerateVerifyTokenEmailRestOK() throws Exception { + given(verifyTokenService.setSidType("EMAIL")).willReturn(SidType.EMAIL); + given(verifyTokenService.convertRequestSid(SidType.EMAIL, EMAIL)).willReturn(EMAIL); + given(verifyCodeGenerator.generateVerifyCode(6)).willReturn(VERIFY_CODE); + given(verifyTokenService.generateVerifyToken(EMAIL, SidType.EMAIL, VERIFY_CODE)).willReturn("token"); + given(verifyTokenService.verifyCodeSend(EMAIL, VERIFY_CODE, SidType.EMAIL, SendMethod.IDLE)).willReturn(null); + + mockMvc.perform(get("/tokens?sid=" + EMAIL + "&sid_type=" + SidType.EMAIL)).andExpect(status().isOk()) + .andExpect(jsonPath("$.token_type", is("CODE"))); + } + @Test public void testGenerateVerifyTokenRestVerifyCodeNotSent() throws Exception { given(verifyCodeGenerator.generateVerifyCode(6)).willReturn(VERIFY_CODE); @@ -84,6 +98,16 @@ public class VerifyTokenControllerTest { .andExpect(status().isInternalServerError()); } + @Test + public void testGenerateVerifyTokenRestVerifyCodeNotSentEmail() throws Exception { + given(verifyCodeGenerator.generateVerifyCode(6)).willReturn(VERIFY_CODE); + given(verifyTokenService.generateVerifyToken(EMAIL, SidType.EMAIL, VERIFY_CODE)).willReturn("VerifyToken"); + given(verifyCodeGenerator.sendVerifyCodeViaEmail(EMAIL, VERIFY_CODE)).willReturn(false); + + mockMvc.perform(get("/tokens?sid=" + EMAIL + "&sid_type=" + SidType.EMAIL)) + .andExpect(status().isInternalServerError()); + } + @Test public void testGenerateVerifyTokenRestSidInvalid() throws Exception { given(validator.verifyTokenRequestValidate(SidType.PHONE, SID_INVALID, SEND_VIA)) @@ -98,6 +122,20 @@ public class VerifyTokenControllerTest { .andExpect(status().isBadRequest()); } + @Test + public void testGenerateVerifyTokenRestSidEmailInvalid() throws Exception { + given(validator.verifyTokenRequestValidate(SidType.EMAIL, EMAIL_INVALID, SendMethod.IDLE)) + .willReturn(new ImmutablePair(Cause.EMAIL_INVALID, "Email is invalid!")); + given(verifyTokenService.setSidType("EMAIL")).willReturn(SidType.EMAIL); + given(verifyTokenService.setSendMethod(null)).willReturn(SendMethod.IDLE); + given(verifyTokenService.convertRequestSid(SidType.EMAIL, EMAIL_INVALID)).willReturn(EMAIL_INVALID); + given(verifyCodeGenerator.generateVerifyCode(6)).willReturn(VERIFY_CODE); + given(verifyTokenService.generateVerifyToken(EMAIL, SidType.EMAIL, VERIFY_CODE)).willReturn("token"); + + mockMvc.perform(get("/tokens?sid=" + EMAIL_INVALID + "&sid_type=" + SidType.EMAIL)) + .andExpect(status().isBadRequest()); + } + @Test public void testGenerateVerifyTokenNullInternalServerError() throws Exception { given(verifyCodeGenerator.generateVerifyCode(6)).willReturn(VERIFY_CODE);