Skip to content

Commit 43c0cd0

Browse files
committed
Add a secret to the AccessToken
1 parent a978653 commit 43c0cd0

12 files changed

Lines changed: 80 additions & 33 deletions

File tree

docs/api.md

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,16 @@ the [International data](customization.md#international-data)
3333
customization section.
3434

3535
For external services, authentication should instead be performed with using a [service account](service-accounts.md)
36-
with a secret access token.
36+
with an access token and a secret.
3737

3838
```http
3939
GET /api/** HTTP/1.1
40-
Authorization: Bearer {accessToken}
40+
Authorization: Bearer {accessToken}:{secret}
4141
```
4242

43+
Tokens created prior to the 1.6 release may omit the `:{secret}` part. We highly recommend that you revoke and
44+
regenerate new tokens with secrets.
45+
4346
Passing the authorization token via `auth` query parameter is deprecated as of 1.4.0.
4447

4548
```http
@@ -160,7 +163,6 @@ GET /api/ontologies/{ontologyName}/terms HTTP/1.1
160163

161164
- `page` the page to query starting from zero to `totalPages`
162165

163-
164166
## List specific terms in a category/ontology
165167

166168
!!! note
@@ -171,8 +173,10 @@ GET /api/ontologies/{ontologyName}/terms HTTP/1.1
171173
GET /api/ontologies/{ontologyName}/terms?ontologyTermIds HTTP/1.1
172174
```
173175

174-
To retrieve specific terms, you may use `ontologyTermIds` query parameter and pass it as many time as you want. The output is
175-
not paginated and the `page` parameter from [List all terms in a category/ontology](#list-all-terms-in-a-categoryontology) is ignored.
176+
To retrieve specific terms, you may use `ontologyTermIds` query parameter and pass it as many time as you want. The
177+
output is
178+
not paginated and the `page` parameter
179+
from [List all terms in a category/ontology](#list-all-terms-in-a-categoryontology) is ignored.
176180

177181
## Retrieve a single category/ontology term
178182

src/main/java/ubc/pavlab/rdp/controllers/AdminController.java

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ public ModelAndView viewCreateServiceAccount() {
114114
}
115115

116116
@PostMapping(value = "/admin/create-service-account")
117-
public Object createServiceAccount( @Validated(User.ValidationServiceAccount.class) User user, BindingResult bindingResult ) {
117+
public Object createServiceAccount( @Validated(User.ValidationServiceAccount.class) User user, BindingResult bindingResult, RedirectAttributes redirectAttributes ) {
118118
String serviceEmail = user.getEmail() + '@' + siteSettings.getHostUrl().getHost();
119119

120120
if ( userService.findUserByEmailNoAuth( serviceEmail ) != null ) {
@@ -136,9 +136,13 @@ public Object createServiceAccount( @Validated(User.ValidationServiceAccount.cla
136136
profile.setContactEmailVerified( false );
137137
user.setProfile( profile );
138138

139-
user = userService.createServiceAccount( user );
139+
UserService.ServiceAccountAndAccessToken userAndAccessToken = userService.createServiceAccount( user );
140140

141-
return "redirect:/admin/users/" + user.getId();
141+
redirectAttributes.addFlashAttribute( "message", String.format( "Created service account with token %s and secret %s.",
142+
userAndAccessToken.getAccessTokenWithRawSecret().getAccessToken().getToken(),
143+
userAndAccessToken.getAccessTokenWithRawSecret().getRawSecret() ) );
144+
145+
return "redirect:/admin/users/" + userAndAccessToken.getUser().getId();
142146
}
143147

144148
@PostMapping(value = "/admin/users/{user}/roles")
@@ -184,8 +188,8 @@ public Object createAccessTokenForUser( @PathVariable(required = false) User use
184188
return new ModelAndView( "error/404", HttpStatus.NOT_FOUND )
185189
.addObject( "message", messageSource.getMessage( "AdminController.userNotFoundById", null, locale ) );
186190
}
187-
AccessToken accessToken = userService.createAccessTokenForUser( user );
188-
redirectAttributes.addFlashAttribute( "message", MessageFormat.format( "Successfully created an access token {0}.", accessToken.getToken() ) );
191+
UserService.AccessTokenWithRawSecret accessToken = userService.createAccessTokenForUser( user );
192+
redirectAttributes.addFlashAttribute( "message", MessageFormat.format( "Successfully created an access token {0} with secret {1}.", accessToken.getAccessToken().getToken(), accessToken.getRawSecret() ) );
189193
return "redirect:/admin/users/" + user.getId();
190194
}
191195

src/main/java/ubc/pavlab/rdp/model/AccessToken.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ public class AccessToken extends Token implements UserContent {
2828
@JoinColumn(name = "user_id", nullable = false)
2929
private User user;
3030

31+
private String secret;
32+
3133
@CreatedDate
3234
private Instant createdAt;
3335

src/main/java/ubc/pavlab/rdp/security/authentication/TokenBasedAuthentication.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,21 @@
55
public class TokenBasedAuthentication extends AbstractAuthenticationToken {
66

77
private final String token;
8+
private final String secret;
89

9-
public TokenBasedAuthentication( String token ) {
10+
public TokenBasedAuthentication( String token, String secret ) {
1011
super( null );
1112
this.token = token;
13+
this.secret = secret;
1214
}
1315

1416
@Override
1517
public Object getCredentials() {
16-
return token;
18+
return secret;
1719
}
1820

1921
@Override
2022
public Object getPrincipal() {
21-
return null;
23+
return token;
2224
}
2325
}

src/main/java/ubc/pavlab/rdp/security/authentication/TokenBasedAuthenticationConverter.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ class TokenBasedAuthenticationConverter implements AuthenticationConverter {
1616
public final TokenBasedAuthentication convert( HttpServletRequest request ) throws IllegalArgumentException {
1717
String authorizationHeader = request.getHeader( "Authorization" );
1818
String authToken = request.getParameter( "auth" );
19+
String secret;
1920
if ( authToken == null && authorizationHeader != null ) {
2021
String[] pieces = authorizationHeader.split( " ", 2 );
2122
if ( pieces[0].equalsIgnoreCase( "Bearer" ) ) {
@@ -28,6 +29,13 @@ public final TokenBasedAuthentication convert( HttpServletRequest request ) thro
2829
return null; /* unsupported authentication scheme */
2930
}
3031
}
31-
return authToken != null ? new TokenBasedAuthentication( authToken ) : null;
32+
if ( authToken != null && authToken.contains( ":" ) ) {
33+
String[] pieces = authToken.split( ":", 2 );
34+
authToken = pieces[0];
35+
secret = pieces[1];
36+
} else {
37+
secret = null;
38+
}
39+
return authToken != null ? new TokenBasedAuthentication( authToken, secret ) : null;
3240
}
3341
}

src/main/java/ubc/pavlab/rdp/security/authentication/TokenBasedAuthenticationManager.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ public TokenBasedAuthenticationManager( UserService userService, ApplicationSett
3131

3232
@Override
3333
public Authentication authenticate( Authentication authentication ) throws AuthenticationException {
34-
String authToken = (String) authentication.getCredentials();
34+
String authToken = (String) authentication.getPrincipal();
35+
String secret = (String) authentication.getCredentials();
3536
User u;
3637
if ( applicationSettings.getIsearch().getAuthTokens().contains( authToken ) ) {
3738
// remote admin authentication
@@ -42,7 +43,7 @@ public Authentication authenticate( Authentication authentication ) throws Authe
4243
} else {
4344
// authentication via access token
4445
try {
45-
u = userService.findUserByAccessTokenNoAuth( authToken );
46+
u = userService.findUserByAccessTokenNoAuth( authToken, secret );
4647
} catch ( ExpiredTokenException e ) {
4748
throw new CredentialsExpiredException( "API token is expired.", e );
4849
} catch ( TokenException e ) {

src/main/java/ubc/pavlab/rdp/services/UserService.java

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package ubc.pavlab.rdp.services;
22

3+
import lombok.Value;
34
import org.springframework.data.domain.Page;
45
import org.springframework.data.domain.Pageable;
56
import org.springframework.security.authentication.BadCredentialsException;
@@ -26,7 +27,13 @@ public interface UserService {
2627

2728
User createAdmin( User admin );
2829

29-
User createServiceAccount( User user );
30+
ServiceAccountAndAccessToken createServiceAccount( User user );
31+
32+
@Value
33+
class ServiceAccountAndAccessToken {
34+
User user;
35+
AccessTokenWithRawSecret accessTokenWithRawSecret;
36+
}
3037

3138
List<Role> findAllRoles();
3239

@@ -61,7 +68,7 @@ public interface UserService {
6168

6269
User findUserByEmailNoAuth( String email );
6370

64-
User findUserByAccessTokenNoAuth( String accessToken ) throws TokenException;
71+
User findUserByAccessTokenNoAuth( String accessToken, String secret ) throws TokenException;
6572

6673
/**
6774
* Anonymize the given user.
@@ -113,7 +120,13 @@ public interface UserService {
113120

114121
void revokeAccessToken( AccessToken accessToken );
115122

116-
AccessToken createAccessTokenForUser( User user );
123+
AccessTokenWithRawSecret createAccessTokenForUser( User user );
124+
125+
@Value
126+
class AccessTokenWithRawSecret {
127+
AccessToken accessToken;
128+
String rawSecret;
129+
}
117130

118131
Optional<User> getRemoteSearchUser();
119132

src/main/java/ubc/pavlab/rdp/services/UserServiceImpl.java

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -143,13 +143,13 @@ public User createAdmin( User admin ) {
143143
@Override
144144
@Secured("ROLE_ADMIN")
145145
@Transactional
146-
public User createServiceAccount( User user ) {
146+
public ServiceAccountAndAccessToken createServiceAccount( User user ) {
147147
user.setPassword( bCryptPasswordEncoder.encode( createSecureRandomToken() ) );
148148
Role serviceAccountRole = roleRepository.findByRole( "ROLE_SERVICE_ACCOUNT" );
149149
user.getRoles().add( serviceAccountRole );
150150
user = userRepository.save( user );
151-
createAccessTokenForUser( user );
152-
return user;
151+
AccessTokenWithRawSecret accessToken = createAccessTokenForUser( user );
152+
return new ServiceAccountAndAccessToken( user, accessToken );
153153
}
154154

155155
@Override
@@ -320,7 +320,7 @@ public User findUserByEmailNoAuth( String email ) {
320320
}
321321

322322
@Override
323-
public User findUserByAccessTokenNoAuth( String accessToken ) throws TokenException {
323+
public User findUserByAccessTokenNoAuth( String accessToken, String secret ) throws TokenException {
324324
AccessToken token = accessTokenRepository.findByToken( accessToken );
325325
if ( token == null ) {
326326
return null;
@@ -329,6 +329,9 @@ public User findUserByAccessTokenNoAuth( String accessToken ) throws TokenExcept
329329
// token is expired
330330
throw new ExpiredTokenException( "Token is expired." );
331331
}
332+
if ( token.getSecret() != null && !bCryptPasswordEncoder.matches( secret, token.getSecret() ) ) {
333+
throw new TokenException( "Provided secret does not match the token." );
334+
}
332335
return token.getUser();
333336
}
334337

@@ -386,11 +389,13 @@ public void revokeAccessToken( AccessToken accessToken ) {
386389
}
387390

388391
@Override
389-
public AccessToken createAccessTokenForUser( User user ) {
392+
public AccessTokenWithRawSecret createAccessTokenForUser( User user ) {
390393
AccessToken token = new AccessToken();
391394
token.updateToken( createSecureRandomToken() );
392395
token.setUser( user );
393-
return accessTokenRepository.save( token );
396+
String secret = createSecureRandomToken();
397+
token.setSecret( bCryptPasswordEncoder.encode( secret ) );
398+
return new AccessTokenWithRawSecret( accessTokenRepository.save( token ), secret );
394399
}
395400

396401
@Override
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
alter table access_token
2+
add column secret varchar(255); -- may be null for pre-1.6 tokens

src/test/java/ubc/pavlab/rdp/controllers/AdminControllerTest.java

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
2828
import org.springframework.security.test.context.support.WithMockUser;
2929
import org.springframework.test.context.TestPropertySource;
30-
import org.springframework.test.context.junit4.SpringRunner;
3130
import org.springframework.test.web.servlet.MockMvc;
3231
import org.springframework.test.web.servlet.MvcResult;
3332
import org.springframework.util.LinkedMultiValueMap;
@@ -181,13 +180,17 @@ public void givenLoggedIn_whenCreateServiceAccount_thenRedirect3xx() throws Exce
181180
when( userService.createServiceAccount( any() ) ).thenAnswer( answer -> {
182181
User createdUser = answer.getArgument( 0, User.class );
183182
createdUser.setId( 1 );
184-
return createdUser;
183+
AccessToken accessToken = new AccessToken();
184+
accessToken.setToken( "1234" );
185+
return new UserService.ServiceAccountAndAccessToken( createdUser, new UserService.AccessTokenWithRawSecret( accessToken, "5678" ) );
185186
} );
186187
mvc.perform( post( "/admin/create-service-account" )
187188
.param( "profile.name", "Service Account" )
188189
.param( "email", "service-account" ) )
189190
.andExpect( status().is3xxRedirection() )
190-
.andExpect( redirectedUrl( "/admin/users/1" ) );
191+
.andExpect( redirectedUrl( "/admin/users/1" ) )
192+
.andExpect( flash().attribute( "message", containsString( "1234" ) ) )
193+
.andExpect( flash().attribute( "message", containsString( "5678" ) ) );
191194
ArgumentCaptor<User> captor = ArgumentCaptor.forClass( User.class );
192195
verify( userService ).createServiceAccount( captor.capture() );
193196
assertThat( captor.getValue() )
@@ -200,12 +203,15 @@ public void givenLoggedIn_whenCreateServiceAccount_thenRedirect3xx() throws Exce
200203
public void givenLoggedIn_whenCreateAccessToken_thenRedirect3xx() throws Exception {
201204
User user = createUser( 1 );
202205
AccessToken accessToken = TestUtils.createAccessToken( 1, user, "1234" );
206+
UserService.AccessTokenWithRawSecret accessTokenWithSecret = new UserService.AccessTokenWithRawSecret( accessToken, "5678" );
203207
when( userService.findUserById( 1 ) ).thenReturn( user );
204-
when( userService.createAccessTokenForUser( user ) ).thenReturn( accessToken );
208+
when( userService.createAccessTokenForUser( user ) ).thenReturn( accessTokenWithSecret );
205209
when( roleRepository.findByRole( "ROLE_USER" ) ).thenReturn( createRole( 1, "ROLE_USER" ) );
206210
mvc.perform( post( "/admin/users/{user}/create-access-token", user.getId() ) )
207211
.andExpect( status().is3xxRedirection() )
208-
.andExpect( redirectedUrl( "/admin/users/1" ) );
212+
.andExpect( redirectedUrl( "/admin/users/1" ) )
213+
.andExpect( flash().attribute( "message", containsString( "1234" ) ) )
214+
.andExpect( flash().attribute( "message", containsString( "5678" ) ) );
209215
verify( userService ).createAccessTokenForUser( user );
210216
}
211217

0 commit comments

Comments
 (0)