Using Apache HttpClient to retrieve Cognito Token for Api Gateway
To secure your Api Gateway, there is the option to add Cognito as Authorizer. It is very easy to setup especially if you will follow the GUI instruction from console.
This is a very common setup: you have your Cognito User Pool where you have create the UI to admit clients to login and check their credentials. The UI can be easily managed because you can integrate it and setup your home Application as redirect URI, so they can be see the home page right after login submit.
Now let's suppose you have an Api Gateway and the same user pool from Cognito is used to allow users to access that. This means that:
- users have to insert credentials
- obtain the auth code
- call the oauth2
- obtain the token
- add it to Authorization header
Usually we do this using SDK from AWS, but I want to experiment an alternative way of do this.
POSTMAN
If you check on the internet you will see that there are many examples of Postman Cognito Oauth, where you can set up all the configuration and click on "generate token": this will open a new browser page on your way, let you introduce the credentials and then retrieve the auth code.
Then the auth code is used for the step "obtain the token".
But hey, is there a way to do this without open a browser? Let's see if it is possible
APACHE HTTP
To call each endpoint it is important to use Apache Http client (version 5 the more recent) which can give us the ability to add headers, parameter, extract information and so on for each request.
So our project will have this dependency
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
<version>5.2</version>
</dependency>
Now we will use just CloseableHttpClient for every call, we will just manage headers and body.
XSRF TOKEN
The firs call we need to do is the one to get the XSRF token, which is the one used in the "Insert Credentials" page.
Supposing to have the cognito domain like
https://mydomain.auth.us-east-1.amazoncognito.comwhere mydomain is what you set in Cognito configuration
https://mydomain.auth.us-east-1.amazoncognito.com/loginAnd you need to call it passing the parameters
URIBuilder uriBuilder = new URIBuilder(uri);
uriBuilder.addParameter("client_id", client_id);
uriBuilder.addParameter("redirect_uri", redirect_uri);
uriBuilder.addParameter("scope", "openid");
uriBuilder.addParameter("response_type", "code");
Scope and response_type are of course "fixed", while client_id and redirect_uri depends on your configuration (client_id are generated by AWS, while redirect_uri are configured by you).
This first call will return a cookie that you can find in your header
public class XsrfRetriever {
private final HttpGet request;
public XsrfRetriever(URI uri) throws URISyntaxException {
request = new HttpGet(uri);
}
public String retrieveToken() throws IOException {
CloseableHttpClient clientGet = HttpClients.createDefault();
HttpResponse responseGet = clientGet.execute(request, new EntireResponseExtractor());
String setCookie = responseGet.getFirstHeader("Set-Cookie").getValue();
return Arrays.stream(setCookie.split(";")).filter(s -> s.contains("XSRF-TOKEN")).
findFirst().get();
}
}
So passing the uri built before to this class, you can obtain the token.
AUTH CODE
Now we can simulate a login using the xsrf token obtained in the previous step
This will be a Post where:
- the URI will be the same as before
- the xsrf token is used as cookie header
- username and password are sent as body parameter with _csrf which is the cookie value of xsrf-token
The result should be a 302 with a location header that contains an url build as "redirecturi?code=XXXX" where XXXX is the code we need for the next step
public class AuthCodeRetriever {
private static final Logger logger = LogManager.getLogger();
private HttpPostClient httpPostClient;
private String redirectUri;
public AuthCodeRetriever(URI finalURI, String username, String password, String xsrfToken,
String redirectUri){
httpPostClient = HttpPostClient.HttpPostClientBuilder.init().withUri(finalURI)
.addHeader("cookie", xsrfToken)
.addHeader("accept", "application/json")
.addHeader("content-type", "application/x-www-form-urlencoded")
.addParameter("username", username)
.addParameter("password", password)
.addParameter("_csrf", xsrfToken.split("=")[1]).build();
this.redirectUri = redirectUri;
}
public Optional<String> retrieveCode() throws IOException, ProtocolException {
String location = httpPostClient.executeAndReturnHttpResponse()
.getHeader("location").getValue();
if (location != null && location.contains(redirectUri)) {
URI extractFromLocation = URI.create(location);
String authCode = extractFromLocation.getQuery().split("&")[0].split("=")[1];
logger.debug("Auth code is " + authCode);
return Optional.of(authCode);
} else return Optional.empty();
}
}
This class add the essential header and parameter to the HttpPost and retrieve the code from the location header.
OAUTH TOKEN
Now the last step. We need to perform a POST to the oauth token url which is in form of
https://mydomain.auth.us-east-1.amazoncognito.com/oauth2/tokenand this post will have:
- the csrf cookie as header
- the body parameter that contains
- the client_id already mentioned before
- the redirect_uri already mentioned before
- the authCode retrieved in the previous step as "code"
- the grant_type as "authorization_code"
This class will perform this as well
public class TokenAuthRetriever {
private final HttpPostClient httpPostClient;
public TokenAuthRetriever (URI uri, String xsrfToken, String authCode,
String redirect_uri, String client_id){
httpPostClient = HttpPostClient.HttpPostClientBuilder.init().withUri(uri)
.addHeader("cookie", xsrfToken)
.addHeader("accept", "application/json")
.addHeader("content-type", "application/x-www-form-urlencoded")
.addParameter("grant_type", "authorization_code")
.addParameter("code", authCode)
.addParameter("redirect_uri", redirect_uri)
.addParameter("client_id", client_id).build();
}
public TokenContainer retrieveToken() throws IOException {
String json = httpPostClient.executeAndReturnString();
ObjectMapper objectMapper = new ObjectMapper();
return objectMapper.readValue(json, TokenContainer.class);
}
}
In the final step instead of returning a simple String it converts it into a TokenContainer, which is an object that fits the json structure
public class TokenContainer {
private String id_token;
private String access_token;
private String refresh_token;
private int expires_in;
private String token_type;
...
...
}
With this last object we can use "id_token" as Authorization for last call
FINAL CALL
To verify that everything is working fine we perform a call to the apigateway passing the token retrieved
HttpGet requestGateway = new HttpGet(API_GATEWAY_URL);
CloseableHttpClient clientGetGateway = HttpClients.createDefault();
requestGateway.addHeader("Authorization", id_token);
HttpResponse responseGateway = clientGetGateway.execute(requestGateway,
new EntireResponseExtractor());
logger.debug("Calling gateway and obtain {}", responseGateway.getCode());
This will print
DEBUG org.example.Main - Calling gateway and obtain 200
MAIN PROGRAM
The main program will be something like
URI uri = UriTokenBuilder.build(URI_BASE, CLIENT_ID, REDIRECT_URI);
String xsrfToken = new XsrfRetriever(uri).retrieveToken();
Optional<String> optionalAuthCode = new AuthCodeRetriever(uri, MAIL, PASSWORD,
xsrfToken, REDIRECT_URI).retrieveCode();
if (optionalAuthCode.isPresent()) {
String id_token = new TokenAuthRetriever(URI_OAUTH_URL, xsrfToken, optionalAuthCode.get(),
REDIRECT_URI, CLIENT_ID).retrieveToken().getId_token();
logger.debug("ID TOKEN is " + id_token);
HttpGet requestGateway = new HttpGet(API_GATEWAY_URL);
CloseableHttpClient clientGetGateway = HttpClients.createDefault();
requestGateway.addHeader("Authorization", id_token);
HttpResponse responseGateway = clientGetGateway.execute(requestGateway,
new EntireResponseExtractor());
logger.debug("Calling gateway and obtain {}", responseGateway.getCode());
}
CONCLUSION
There are many way of interacting with AWS services: using the SDK will be always the preferred one, but sometimes we need to verify if we are able to call them directly using HTTP interaction.
Remember that they are all API
Commenti
Posta un commento