Skip to content

Commit

Permalink
Merge pull request #318 from RADAR-base/remove_client_secret_field_fr…
Browse files Browse the repository at this point in the history
…om_metatoken

Remove client secret field from metatoken
  • Loading branch information
blootsvoets authored Aug 29, 2018
2 parents 8669397 + d402bcd commit 2ddb145
Show file tree
Hide file tree
Showing 6 changed files with 66 additions and 66 deletions.
42 changes: 27 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,37 +121,48 @@ clients for an example.
- If your client is `dynamic_registration` enabled, the QR code generated by `Pair app` feature will contain a short-living URL. By doing a `GET` request on that URL the `refresh-token` and related meta-data can be fetched.
- If you want to prevent an OAuth client from being altered through the UI, you can add a key `{"protected": true}` in the `additional_information` map.

If the app is paired via the Pair App dialog, the QR code that will be scanned contains a refresh token.
```
{
"refreshToken": "..."
}
```
The app can use that refresh token to get new access and refresh tokens:
If the app is paired via the Pair App dialog, the QR code that will be scanned contains a short-lived URL, e.g. `https://radar-base-url.org/api/meta-token/bMUkowOmTOci`

Your app should access the URL, where it will receive an OAuth2
refresh token as well as the platform's base URL and a URL to the privacy policy. No authorization
is required to access this URL. **Important:** For security reasons, the information at this URL can
only be accessed once. Once it has been accessed it can not be retrieved again.

The app can use that refresh token to get new access and refresh tokens by doing the following HTTP
request to the base URL, using HTTP basic authentication with your OAuth client ID as username, and
an empty password.
```
POST MyId:MySecret /oauth/token
POST /oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token&refresh_token=...
grant_type=refresh_token&refresh_token=<refresh_token>
```
This will respond with the access token and refresh token:
This will respond with at least the access token and refresh token:
```json
{
"access_token": "...",
"refresh_token": "...",
"expires_in": 14400
}
```
For the next request, you need to use the `refresh_token` that was returned rather than the one in the QR code, since refresh tokens are valid only once.
Both tokens are valid for a limited time only. When the access token runs out, you will need to
perform another request like the one above, but you need to use the new `refresh_token`, since
refresh tokens are valid only once.

The code grant flow for OAuth2 clients can be the following:
1. Ask user confirmation for your app:
```
GET /oauth/confirm_access?client_id=MyId&client_secret=MySecret&grant_type=code&redirect_uri=https://my.example.com/oauth_redirect
GET /oauth/confirm_access?client_id=MyId&grant_type=code&redirect_uri=https://my.example.com/oauth_redirect
```
This needs to be done from a interactive web view, either a browser or a web window. If the user approves, this will redirect to `https://my.example.com/oauth_redirect?code=abcdef`. In Android, with [https://appauth.io](AppAuth library), the URL could be `com.example.my://oauth_redirect` for the `com.example.my` app.
2. Request a token for you app:
where you replace `MyId` with your OAuth client id. This needs to be done from a interactive
web view, either a browser or a web window. If the user approves, this will redirect to
`https://my.example.com/oauth_redirect?code=abcdef`. In Android, with [https://appauth.io]
(AppAuth library), the URL could be `com.example.my://oauth_redirect` for the `com.example.my`
app.
2. Request a token for your app by doing a POST, again with HTTP basic authentication with as
username your OAuth client id, and leaving the password empty:
```
POST MyId:MySecret /oauth/token
POST /oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&code=abcdef&redirect_uri=https://my.example.com/oauth_redirect
Expand Down Expand Up @@ -347,3 +358,4 @@ The resulting file can be imported into the [Swagger editor], or used with [Swag
[OpenAPI]: https://www.openapis.org/
[Swagger editor]: http://editor.swagger.io/
[Swagger codegen]: https://swagger.io/swagger-codegen/
[OAuth2 spec]: https://tools.ietf.org/html/rfc6749#section-9
8 changes: 4 additions & 4 deletions src/main/docker/etc/config/oauth_client_details.csv
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
client_id;resource_ids;client_secret;scope;authorized_grant_types;redirect_uri;authorities;access_token_validity;refresh_token_validity;additional_information;autoapprove
pRMT;res_ManagementPortal,res_gateway;secret;MEASUREMENT.CREATE,SUBJECT.UPDATE,SUBJECT.READ,PROJECT.READ,SOURCETYPE.READ,SOURCE.READ,SOURCETYPE.READ,SOURCEDATA.READ,USER.READ,ROLE.READ;refresh_token,authorization_code;;;43200;7948800;{"dynamic_registration": true};
aRMT;res_ManagementPortal,res_gateway;secret;MEASUREMENT.CREATE,SUBJECT.UPDATE,SUBJECT.READ,PROJECT.READ,SOURCETYPE.READ,SOURCE.READ,SOURCETYPE.READ,SOURCEDATA.READ,USER.READ,ROLE.READ;refresh_token,authorization_code;;;43200;7948800;{"dynamic_registration": true};
THINC-IT;res_ManagementPortal,res_gateway;secret;MEASUREMENT.CREATE,SUBJECT.UPDATE,SUBJECT.READ,PROJECT.READ,SOURCETYPE.READ,SOURCE.READ,SOURCETYPE.READ,SOURCEDATA.READ,USER.READ,ROLE.READ;refresh_token,authorization_code;;;43200;7948800;{"dynamic_registration": true};
pRMT;res_ManagementPortal,res_gateway;;MEASUREMENT.CREATE,SUBJECT.UPDATE,SUBJECT.READ,PROJECT.READ,SOURCETYPE.READ,SOURCE.READ,SOURCETYPE.READ,SOURCEDATA.READ,USER.READ,ROLE.READ;refresh_token,authorization_code;;;43200;7948800;{"dynamic_registration": true};
aRMT;res_ManagementPortal,res_gateway;;MEASUREMENT.CREATE,SUBJECT.UPDATE,SUBJECT.READ,PROJECT.READ,SOURCETYPE.READ,SOURCE.READ,SOURCETYPE.READ,SOURCEDATA.READ,USER.READ,ROLE.READ;refresh_token,authorization_code;;;43200;7948800;{"dynamic_registration": true};
THINC-IT;res_ManagementPortal,res_gateway;;MEASUREMENT.CREATE,SUBJECT.UPDATE,SUBJECT.READ,PROJECT.READ,SOURCETYPE.READ,SOURCE.READ,SOURCETYPE.READ,SOURCEDATA.READ,USER.READ,ROLE.READ;refresh_token,authorization_code;;;43200;7948800;{"dynamic_registration": true};
radar_restapi;res_ManagementPortal;secret;SUBJECT.READ,PROJECT.READ,SOURCE.READ,SOURCETYPE.READ;client_credentials;;;43200;259200;{};
radar_redcap_integrator;res_ManagementPortal;secret;PROJECT.READ,SUBJECT.CREATE,SUBJECT.READ,SUBJECT.UPDATE;client_credentials;;;43200;259200;{};
radar_dashboard;res_ManagementPortal,res_RestApi;secret;SUBJECT.READ,PROJECT.READ,SOURCE.READ,SOURCETYPE.READ;refresh_token,authorization_code;;;43200;259200;{};
radar_dashboard;res_ManagementPortal,res_RestApi;;SUBJECT.READ,PROJECT.READ,SOURCE.READ,SOURCETYPE.READ;refresh_token,authorization_code;;;43200;259200;{};
50 changes: 15 additions & 35 deletions src/main/java/org/radarcns/management/service/MetaTokenService.java
Original file line number Diff line number Diff line change
@@ -1,32 +1,29 @@
package org.radarcns.management.service;

import static org.radarcns.management.web.rest.errors.EntityName.META_TOKEN;
import static org.radarcns.management.web.rest.errors.EntityName.OAUTH_CLIENT;

import java.net.MalformedURLException;
import java.net.URL;
import java.time.Instant;
import java.util.Collections;
import java.util.Optional;

import javax.validation.ConstraintViolationException;

import org.radarcns.management.config.ManagementPortalProperties;
import org.radarcns.management.domain.MetaToken;
import org.radarcns.management.domain.Subject;
import org.radarcns.management.repository.MetaTokenRepository;
import org.radarcns.management.service.dto.TokenDTO;
import org.radarcns.management.web.rest.errors.RequestGoneException;
import org.radarcns.management.web.rest.errors.NotFoundException;
import org.radarcns.management.web.rest.errors.ErrorConstants;
import org.radarcns.management.web.rest.errors.NotFoundException;
import org.radarcns.management.web.rest.errors.RequestGoneException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.security.oauth2.provider.ClientDetails;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.validation.ConstraintViolationException;
import java.net.MalformedURLException;
import java.net.URL;
import java.time.Instant;
import java.util.Collections;
import java.util.Optional;

import static org.radarcns.management.web.rest.errors.EntityName.META_TOKEN;

/**
* Created by nivethika.
*
Expand All @@ -48,9 +45,6 @@ public class MetaTokenService {
@Autowired
private SubjectService subjectService;

@Autowired
private OAuthClientService oAuthClientService;

/**
* Save a metaToken.
*
Expand All @@ -68,27 +62,15 @@ public MetaToken save(MetaToken metaToken) {
* @param tokenName the id of the entity
* @return the entity
*/
public TokenDTO fetchToken(String tokenName) throws
MalformedURLException {
public TokenDTO fetchToken(String tokenName) throws MalformedURLException {
log.debug("Request to get Token : {}", tokenName);
MetaToken fetchedToken = getToken(tokenName);
TokenDTO result = null;
// process the response if the token is not fetched or not expired
if (!fetchedToken.isFetched() && Instant.now().isBefore(fetchedToken.getExpiryDate())) {
// create response

ClientDetails clientDetails = oAuthClientService.findOneByClientId(fetchedToken
.getClientId());
if (clientDetails != null) {
result = new TokenDTO(fetchedToken.getToken(),
new URL(managementPortalProperties.getCommon().getBaseUrl()),
subjectService.getPrivacyPolicyUrl(fetchedToken.getSubject()),
clientDetails.getClientSecret());
} else {
throw new NotFoundException("Oauth client not found with client-id", OAUTH_CLIENT,
ErrorConstants.ERR_TOKEN_NOT_FOUND,
Collections.singletonMap("client-id", fetchedToken.getClientId()));
}
TokenDTO result = new TokenDTO(fetchedToken.getToken(),
new URL(managementPortalProperties.getCommon().getBaseUrl()),
subjectService.getPrivacyPolicyUrl(fetchedToken.getSubject()));

// change fetched status to true.
fetchedToken.fetched(true);
Expand All @@ -98,8 +80,6 @@ public TokenDTO fetchToken(String tokenName) throws
throw new RequestGoneException("Token already fetched or expired. ",
META_TOKEN, "error.TokenCannotBeSent");
}


}

/**
Expand Down
15 changes: 3 additions & 12 deletions src/main/java/org/radarcns/management/service/dto/TokenDTO.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,17 @@ public class TokenDTO {

private final URL privacyPolicyUrl;

private final String clientSecret;

/**
* Create a meta-token using refreshToken, baseUrl of platform, and privacyPolicyURL for this
* token.
* @param refreshToken refreshToken.
* @param baseUrl baseUrl of the platform
* @param privacyPolicyUrl privacyPolicyUrl for this token.
*/
public TokenDTO(String refreshToken, URL baseUrl, URL privacyPolicyUrl, String clientSecret) {
public TokenDTO(String refreshToken, URL baseUrl, URL privacyPolicyUrl) {
this.refreshToken = refreshToken;
this.baseUrl = baseUrl;
this.privacyPolicyUrl = privacyPolicyUrl;
this.clientSecret = clientSecret;
}

public String getRefreshToken() {
Expand All @@ -38,10 +35,6 @@ public URL getPrivacyPolicyUrl() {
return privacyPolicyUrl;
}

public String getClientSecret() {
return clientSecret;
}

@Override
public boolean equals(Object o) {
if (this == o) {
Expand All @@ -53,14 +46,13 @@ public boolean equals(Object o) {
TokenDTO that = (TokenDTO) o;
return Objects.equals(refreshToken, that.refreshToken)
&& Objects.equals(baseUrl, that.baseUrl)
&& Objects.equals(privacyPolicyUrl, that.privacyPolicyUrl)
&& Objects.equals(clientSecret, that.clientSecret);
&& Objects.equals(privacyPolicyUrl, that.privacyPolicyUrl);
}

@Override
public int hashCode() {

return Objects.hash(refreshToken, baseUrl, privacyPolicyUrl, clientSecret);
return Objects.hash(refreshToken, baseUrl, privacyPolicyUrl);
}

@Override
Expand All @@ -69,7 +61,6 @@ public String toString() {
+ "refreshToken='" + refreshToken
+ ", baseUrl=" + baseUrl
+ ", privacyPolicyUrl=" + privacyPolicyUrl
+ ", clientSecret=" + clientSecret
+ '}';
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ public final class ErrorConstants {
+ ".activeParticipantProjectNotFound";
public static final String ERR_NO_VALID_PRIVACY_POLICY_URL_CONFIGURED = "error"
+ ".noValidPrivacyPolicyUrl";
public static final String ERR_NO_SUCH_CLIENT = "error.noSuchClient";

private ErrorConstants() {
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import org.springframework.http.ResponseEntity;
import org.springframework.http.ResponseEntity.BodyBuilder;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.oauth2.provider.NoSuchClientException;
import org.springframework.transaction.TransactionSystemException;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
Expand Down Expand Up @@ -159,6 +160,21 @@ public ErrorVM processMethodNotSupportedException(HttpRequestMethodNotSupportedE
return new ErrorVM(ErrorConstants.ERR_METHOD_NOT_SUPPORTED, ex.getMessage());
}

/**
* If a client tries to initiate an OAuth flow with a non-existing client, this will
* translate the error into a bad request status. Otherwise we return an internal server
* error status, but it is not a server error.
*
* @param ex the exception
* @return the view-model for the translated exception
*/
@ExceptionHandler(NoSuchClientException.class)
@ResponseBody
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ErrorVM processNoSuchClientException(NoSuchClientException ex) {
return new ErrorVM(ErrorConstants.ERR_NO_SUCH_CLIENT, ex.getMessage());
}

/**
* Generic exception translator.
* @param ex the exception
Expand Down

0 comments on commit 2ddb145

Please sign in to comment.