Skip to content
This repository has been archived by the owner on Feb 7, 2024. It is now read-only.

Commit

Permalink
Permissions update, moved refresh api to separate controller (#150)
Browse files Browse the repository at this point in the history
* add user permissions

* moved refresh api call to separate controller

* updated throttle delay
  • Loading branch information
JuliaGalabut authored Dec 5, 2023
1 parent 7b85c5a commit 9c064c5
Show file tree
Hide file tree
Showing 11 changed files with 248 additions and 27 deletions.
10 changes: 7 additions & 3 deletions oap-ws-account-api/src/main/resources/META-INF/oap-ws-roles.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ roles:
ban:user,
organization:user_apikey,
publisher:manage,
campaign:manage,]
campaign:manage,
user:edit_self
]
ORGANIZATION_ADMIN: [ organization:user_passwd,
organization:store_user,
organization:read,
Expand All @@ -33,13 +35,15 @@ roles:
ban:user,
campaign:manage,
publisher:manage,
organization:user_apikey
organization:user_apikey,
user:edit_self
]
USER: [ user:edit_self,
user:passwd,
account:read,
account:list,
campaign:manage,
user:apikey]
user:apikey
]


2 changes: 1 addition & 1 deletion oap-ws-sso-api/src/main/java/oap/ws/sso/Authenticator.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public interface Authenticator {

Result<Authentication, AuthenticationFailure> authenticate( String email, Optional<String> tfaCode );

Result<Authentication, AuthenticationFailure> refreshToken( String refreshToken );
Result<Authentication, AuthenticationFailure> refreshToken( String refreshToken, Optional<String> currentOrganization );

Optional<Authentication> authenticateTrusted( String email );

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import oap.ws.sso.User;
import oap.ws.sso.UserProvider;
import oap.ws.sso.WsSecurity;
import org.apache.commons.lang3.StringUtils;

import java.util.Arrays;
import java.util.List;
Expand Down Expand Up @@ -124,12 +125,12 @@ private String issuerFromContext( InvocationContext context ) {
}

private boolean hasRealmMismatchError( String organization, boolean useOrganizationLogin, String realmString ) {
boolean organizationNotNull = organization != null;
boolean organizationNotEmpty = !StringUtils.isEmpty( organization );
boolean realmNotEqualOrganization = !realmString.equals( organization );
boolean realmNotEqualSystem = !SYSTEM.equals( realmString );
boolean organizationNotEqualSystem = !SYSTEM.equals( organization );

return organizationNotNull && useOrganizationLogin
return organizationNotEmpty && useOrganizationLogin
&& realmNotEqualOrganization
&& realmNotEqualSystem
&& organizationNotEqualSystem;
Expand Down
9 changes: 0 additions & 9 deletions oap-ws-sso/src/main/java/oap/ws/sso/AuthWS.java
Original file line number Diff line number Diff line change
Expand Up @@ -140,15 +140,6 @@ else if( TOKEN_NOT_VALID == result.getFailureValue() ) {
return notAuthenticatedResponse( UNAUTHORIZED, "User not found", sessionManager.cookieDomain );
}

@WsMethod( method = GET, path = "/refresh" )
public Response refreshToken( @WsParam( from = COOKIE ) String refreshToken ) {
var result = authenticator.refreshToken( refreshToken );
if( result.isSuccess() ) return authenticatedResponse( result.getSuccessValue(),
sessionManager.cookieDomain, sessionManager.cookieExpiration, sessionManager.cookieSecure );
else
return notAuthenticatedResponse( UNAUTHORIZED, "Token is invalid", sessionManager.cookieDomain );
}

@WsMethod( method = GET, path = "/logout" )
public Response logout( @WsParam( from = SESSION ) Optional<User> loggedUser,
Session session ) {
Expand Down
42 changes: 32 additions & 10 deletions oap-ws-sso/src/main/java/oap/ws/sso/JwtUserAuthenticator.java
Original file line number Diff line number Diff line change
Expand Up @@ -63,14 +63,14 @@ public Result<Authentication, AuthenticationFailure> authenticateWithActiveOrgId
if( user.isEmpty() ) {
return Result.failure( AuthenticationFailure.UNAUTHENTICATED );
}
return user.filter( u -> validateUserAcess( u, orgId ) )
return user.filter( u -> validateUserAccess( u, orgId ) )
.map( u -> getAuthenticationTokens( u, orgId ) )
.orElse( Result.failure( AuthenticationFailure.WRONG_ORGANIZATION ) );
}
return Result.failure( AuthenticationFailure.TOKEN_NOT_VALID );
}

private boolean validateUserAcess( User user, String orgId ) {
private boolean validateUserAccess( User user, String orgId ) {
return user.getRoles().containsKey( orgId );
}

Expand Down Expand Up @@ -112,17 +112,39 @@ private Authentication generateTokenWithOrgId( User user, String activeOrgId ) {
return new Authentication( accessToken, refreshToken, user );
}

@Override
public Result<Authentication, AuthenticationFailure> refreshToken( String refreshToken ) {
if( jwtExtractor.verifyToken( refreshToken ) ) {
log.trace( "generating new authentication by refreshToken {} ", refreshToken );
final Optional<Authentication> authentication = userProvider.getUser( jwtExtractor.getUserEmail( refreshToken ) ).map( user ->
new Authentication( jwtTokenGenerator.generateAccessToken( user ), jwtTokenGenerator.generateRefreshToken( user ), user ) );
return authentication.<Result<Authentication, AuthenticationFailure>>map( Result::success ).orElseGet( () -> Result.failure( AuthenticationFailure.UNAUTHENTICATED ) );
public Result<Authentication, AuthenticationFailure> refreshToken( String refreshToken, Optional<String> orgId ) {
if( !jwtExtractor.verifyToken( refreshToken ) ) {
return Result.failure( AuthenticationFailure.TOKEN_NOT_VALID );
}
return Result.failure( AuthenticationFailure.TOKEN_NOT_VALID );
return generateAuthentication( refreshToken, orgId );
}

private Result<Authentication, AuthenticationFailure> generateAuthentication( String refreshToken, Optional<String> orgId ) {
var userEmail = jwtExtractor.getUserEmail( refreshToken );
var user = userProvider.getUser( userEmail );

if( user.isEmpty() ) {
return Result.failure( AuthenticationFailure.UNAUTHENTICATED );
}
return buildAuthentication( user.get(), orgId );
}

private Result<Authentication, AuthenticationFailure> buildAuthentication( User user, Optional<String> orgId ) {
if( orgId.isPresent() && !user.getRoles().containsKey( orgId.get() ) ) {
return Result.failure( AuthenticationFailure.UNAUTHENTICATED );
}
String activeOrgId = orgId.orElse( user.getDefaultOrganization().orElse( "" ) );

var authentication = new Authentication(
jwtTokenGenerator.generateAccessTokenWithActiveOrgId( user, activeOrgId ),
jwtTokenGenerator.generateRefreshToken( user ),
user
);

return Result.success( authentication );
}


@Override
public Optional<Authentication> authenticateTrusted( String email ) {
return userProvider.getUser( email )
Expand Down
63 changes: 63 additions & 0 deletions oap-ws-sso/src/main/java/oap/ws/sso/RefreshWS.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* The MIT License (MIT)
*
* Copyright (c) Open Application Platform Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/

package oap.ws.sso;

import lombok.extern.slf4j.Slf4j;
import oap.ws.Response;
import oap.ws.SessionManager;
import oap.ws.WsMethod;
import oap.ws.WsParam;

import java.util.Optional;

import static oap.http.Http.StatusCode.UNAUTHORIZED;
import static oap.http.server.nio.HttpServerExchange.HttpMethod.GET;
import static oap.ws.WsParam.From.COOKIE;
import static oap.ws.WsParam.From.QUERY;
import static oap.ws.sso.SSO.authenticatedResponse;
import static oap.ws.sso.SSO.notAuthenticatedResponse;

@Slf4j
@SuppressWarnings( "unused" )
public class RefreshWS {

private final Authenticator authenticator;
private final SessionManager sessionManager;

public RefreshWS( Authenticator authenticator, SessionManager sessionManager ) {
this.authenticator = authenticator;
this.sessionManager = sessionManager;
}

@WsMethod( method = GET, path = "/" )
public Response refreshToken( @WsParam( from = COOKIE ) String refreshToken,
@WsParam( from = QUERY ) Optional<String> organizationId ) {
var result = authenticator.refreshToken( refreshToken, organizationId );
if( result.isSuccess() ) return authenticatedResponse( result.getSuccessValue(),
sessionManager.cookieDomain, sessionManager.cookieExpiration, sessionManager.cookieSecure );
else
return notAuthenticatedResponse( UNAUTHORIZED, "Token is invalid", sessionManager.cookieDomain );
}
}
16 changes: 16 additions & 0 deletions oap-ws-sso/src/main/resources/META-INF/oap-module.conf
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,20 @@ services {
]
}
}

refresh-ws {
implementation = oap.ws.sso.RefreshWS
parameters {
authenticator = modules.this.oap-ws-sso-authenticator
sessionManager = modules.oap-ws.session-manager
oauthService = modules.oap-ws-account-social.oauth-service
}
ws-service {
path = refresh
sessionAware = true
interceptors = [
oap-ws-sso-api.oap-ws-sso-throttle-login-interceptor
]
}
}
}
2 changes: 1 addition & 1 deletion oap-ws-testing/src/test/java/oap/ws/sso/AuthWSTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ public void loginWhoami() {

@Test
public void loginResponseTest() {
final TestUser testUser = userProvider().addUser( new TestUser( "[email protected]", "pass", __( "r1", "ADMIN" ) ) );
userProvider().addUser( new TestUser( "[email protected]", "pass", __( "r1", "ADMIN" ) ) );
assertPost( httpUrl( "/auth/login" ), "{ \"email\":\"[email protected]\",\"password\": \"pass\"}" )
.hasCode( Http.StatusCode.OK ).satisfies( resp -> {
Map<String, String> response = Binder.json.unmarshal( Map.class, resp.contentString() );
Expand Down
5 changes: 5 additions & 0 deletions oap-ws-testing/src/test/java/oap/ws/sso/IntegratedTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@ protected TestUserProvider userProvider() {
return kernelFixture.service( "oap-ws-sso-test", TestUserProvider.class );
}

protected JWTExtractor tokenExtractor() {
return kernelFixture.service( "oap-ws-sso-api", JWTExtractor.class );
}


@AfterMethod
public void afterMethod() {
assertLogout();
Expand Down
119 changes: 119 additions & 0 deletions oap-ws-testing/src/test/java/oap/ws/sso/RefreshWSTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/*
* The MIT License (MIT)
*
* Copyright (c) Open Application Platform Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/

package oap.ws.sso;


import oap.json.Binder;
import oap.ws.sso.interceptor.ThrottleLoginInterceptor;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;

import java.util.Map;

import static oap.http.Http.StatusCode.OK;
import static oap.http.Http.StatusCode.UNAUTHORIZED;
import static oap.http.testng.HttpAsserts.assertGet;
import static oap.http.testng.HttpAsserts.assertPost;
import static oap.http.testng.HttpAsserts.httpUrl;
import static oap.util.Pair.__;
import static org.testng.Assert.assertNotEquals;
import static org.testng.AssertJUnit.assertEquals;
import static org.testng.AssertJUnit.assertTrue;

public class RefreshWSTest extends IntegratedTest {


@BeforeMethod
public void beforeMethod() {
kernelFixture.service( "oap-ws-sso-api", ThrottleLoginInterceptor.class ).delay = -1;
}

@Test
public void refreshResponseTest() throws InterruptedException {
userProvider().addUser( new TestUser( "[email protected]", "pass", __( "r1", "ADMIN" ) ) );
final String[] accessToken = new String[1];
final String[] refreshToken = new String[1];
assertPost( httpUrl( "/auth/login" ), "{ \"email\":\"[email protected]\",\"password\": \"pass\"}" )
.hasCode( OK ).satisfies( resp -> {
Map<String, String> response = Binder.json.unmarshal( Map.class, resp.contentString() );
assertTrue( response.containsKey( "accessToken" ) );
assertTrue( response.containsKey( "refreshToken" ) );
accessToken[0] = response.get( "accessToken" );
refreshToken[0] = response.get( "refreshToken" );
} );
Thread.sleep( 2000L );
assertGet( httpUrl( "/refresh" ), Map.of( "organizationId", "r1" ), Map.of() ).satisfies( resp -> {
Map<String, String> response = Binder.json.unmarshal( Map.class, resp.contentString() );
assertTrue( response.containsKey( "accessToken" ) );
assertNotEquals( response.get( "accessToken" ), accessToken[0] );
assertTrue( response.containsKey( "refreshToken" ) );
System.out.println( "RefreshToken in response:" + response.get( "refreshToken" ) );
System.out.println( "RefreshToken previous:" + refreshToken[0] );
assertNotEquals( response.get( "refreshToken" ), refreshToken[0] );
} );
}

@Test
public void refreshResponseWithEWrongOrgIdTest() {
userProvider().addUser( new TestUser( "[email protected]", "pass", __( "r1", "ADMIN" ) ) );
assertPost( httpUrl( "/auth/login" ), "{ \"email\":\"[email protected]\",\"password\": \"pass\"}" )
.hasCode( OK ).satisfies( resp -> {
Map<String, String> response = Binder.json.unmarshal( Map.class, resp.contentString() );
assertTrue( response.containsKey( "accessToken" ) );
assertTrue( response.containsKey( "refreshToken" ) );
} );
assertGet( httpUrl( "/refresh" ), Map.of( "organizationId", "r2" ), Map.of() ).hasCode( UNAUTHORIZED );
}

@Test
public void refreshResponseWithoutOrgIdTest() {
final TestUser testUser = userProvider().addUser( new TestUser( "[email protected]", "pass", Map.of( "r1", "ADMIN", "r2", "USER" ) ) );
testUser.defaultOrganization = "r2";
final String[] refreshToken = new String[1];
assertPost( httpUrl( "/auth/login" ), "{ \"email\":\"[email protected]\",\"password\": \"pass\"}" )
.hasCode( OK ).satisfies( resp -> {
Map<String, String> response = Binder.json.unmarshal( Map.class, resp.contentString() );
assertTrue( response.containsKey( "accessToken" ) );
assertTrue( response.containsKey( "refreshToken" ) );
refreshToken[0] = response.get( "refreshToken" );
} );
assertGet( httpUrl( "/refresh" ) ).hasCode( OK ).satisfies( resp -> {
Map<String, String> response = Binder.json.unmarshal( Map.class, resp.contentString() );
assertTrue( response.containsKey( "accessToken" ) );
assertTrue( response.containsKey( "refreshToken" ) );
final String organizationId = tokenExtractor().getOrganizationId( response.get( "accessToken" ) );
assertEquals( "r2", organizationId );
} );
}


@Test
public void refreshWithExpiredRefreshToken() {
final String expiredToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoidGVzdEB0ZXN0LmlvIiwiaXNzIjoiPGNoYW5nZSBtZT4iLCJleHAiOjE2OTY5MjYzODJ9.VZdySTBEThoTOwB73JMNpBgCjaXGvlmes8_13Bs3dXg";
Map<String, Object> headers = Map.of( "Cookie", "refreshToken=" + expiredToken );
assertGet( httpUrl( "/refresh" ), Map.of(), headers ).hasCode( 401 );
}

}
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@
</distributionManagement>

<properties>
<oap-ws.project.version>21.1.0</oap-ws.project.version>
<oap-ws.project.version>21.1.1</oap-ws.project.version>
<oap.deps.oap.version>21.1.0</oap.deps.oap.version>

<oap.deps.mail.version>21.0.0</oap.deps.mail.version>
Expand Down

0 comments on commit 9c064c5

Please sign in to comment.