Three Legged OAuth
The public API of ImmoScout24 uses the OAuth 1.0a protocol to manage access to user data. To access the resources of a user in our system (e.g. a real estate listing) the user needs to grant permission to your application. In order to gain the permission you need to follow the steps as defined in the OAuth 1.0a protocol.
The protocol involves three different parties:
- Your Application (Consumer)
- Our API (Service Provider)
- Your user who has an account at ImmoScout24 (User)
Main Steps of 3-legged OAuth 1.0a flow
- Get a temporary request token which will later be exchanged for the access token.
- Authorize the request token by sending the user via their browser to our User Authorization URL, where the user is asked to authorize your application. Our server will send the user back to your application with a verification code.
- Exchange the request token and verification code for the access token. The access token can be used to access resources owned by the user.
Important: Each access token belongs to exactly one user. Thus your application needs to manage them accordingly.
Our API provides three endpoints that you need for the exchange:
/restapi/security/oauth/request_token
: This is the Request Token URL where your application gets the request token./restapi/security/oauth/confirm_access
This is the User Authorization URL where you need to send your user to grant access to your application./restapi/security/oauth/access_token
This is the Access Token URL where you can exchange the request token and the verification code for the access token.
Sequence diagram
Below you find a sequence diagram, which shows the process of three-legged OAuth 1.0 :
Code example
We have provided a complete code example which implements the token exchange against our Sandbox service and then makes an authorized request against the realestate API. If you want to run the code, follow the steps in the github repository.
In our example we use Spring Security to manage the local users of the application as well as a library to sign requests according to the OAuth 1.0a definition.
Walkthrough of the example
- It all starts when an Immoscout user requires access to protected resources on our API.
- To initiate the authorization process you need to get a request token from our API.
- Include the callback url to which our API will redirect the user after they have granted access to your application.
- You sign this request with your consumer key and consumer secret.
-
Store the resulting token (the request token) and its secret, you will need them in the following steps!
-
The next step is to send your user to our API with the request token, where they can confirm that your application is allowed to communicate with our API in the name of the user (code line).
- They will have to login at ImmoScout24 (browser interaction) and are presented with an access confirmation dialog.
- After the user has confirmed access, our API will redirect the user to the configured callback endpoint in your application.
In your callback endpoint, you need to check that the access has been granted (check the state parameter).
If the user has confirmed the access, you can extract the verifier from the request.
To obtain the access token, you need to send a request with the request token and verifier. The request needs to be signed with both your consumer secret and the request token secret.
Finally, you can use the access token to fetch data from the realestate API.
The Java Code:
package de.is24.oauth1flow;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth.common.signature.SharedConsumerSecretImpl;
import org.springframework.security.oauth.consumer.BaseProtectedResourceDetails;
import org.springframework.security.oauth.consumer.OAuthConsumerSupport;
import org.springframework.security.oauth.consumer.OAuthConsumerToken;
import org.springframework.security.oauth.consumer.ProtectedResourceDetails;
import org.springframework.security.oauth.consumer.client.CoreOAuthConsumerSupport;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.reactive.function.client.WebClient;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
@RestController
class TestController {
private static final String SANDBOX_URL = "https://rest.sandbox-immobilienscout24.de";
public static final String REQUEST_TOKEN_URL = SANDBOX_URL + "/restapi/security/oauth/request_token";
public static final String ACCESS_TOKEN_URL = SANDBOX_URL + "/restapi/security/oauth/access_token";
public static final String ACCESS_CONFIRMATION_URL = SANDBOX_URL + "/restapi/security/oauth/confirm_access";
private static final String RESOURCE_ENDPOINT_URL = SANDBOX_URL + "/restapi/api/offer/v1.0/user/me/realestate/";
private static final String CLIENT_KEY = "your-client-key";
private static final String CLIENT_SECRET = "your-client-secret";
private static final String IS24_SANDBOX = "is24-sandbox";
public static final String AUTHORIZED = "authorized";
public static final String REJECTED = "rejected";
@Value("http://localhost:${server.port}/callback")
String callbackUrl;
WebClient webClient = WebClient.builder().build();
OAuthConsumerSupport oAuthConsumerSupport = new CoreOAuthConsumerSupport();
ProtectedResourceDetails is24ClientKeyDetails = createIs24ClientKeyDetails();
Map requestTokenRepository = new HashMap<>();
Map accessTokenRepository = new HashMap<>();
ProtectedResourceDetails createIs24ClientKeyDetails() {
BaseProtectedResourceDetails protectedResourceDetails = new BaseProtectedResourceDetails();
protectedResourceDetails.setConsumerKey(CLIENT_KEY);
protectedResourceDetails.setSharedSecret(new SharedConsumerSecretImpl(CLIENT_SECRET));
protectedResourceDetails.setSignatureMethod("HMAC-SHA1");
protectedResourceDetails.setAccessTokenURL(ACCESS_TOKEN_URL);
protectedResourceDetails.setRequestTokenURL(REQUEST_TOKEN_URL);
protectedResourceDetails.setId(IS24_SANDBOX);
return protectedResourceDetails;
}
@GetMapping("/initialize-token-exchange")
public void initializeTokenExchange(HttpServletResponse response, Authentication yourLocalUserAuthentication) throws IOException {
String userName = yourLocalUserAuthentication.getName();
OAuthConsumerToken requestToken = oAuthConsumerSupport.getUnauthorizedRequestToken(is24ClientKeyDetails, callbackUrl);
requestTokenRepository.put(userName, requestToken);
response.sendRedirect(ACCESS_CONFIRMATION_URL + "?oauth_token=" + requestToken.getValue());
}
@GetMapping("/callback")
public void oauthCallback(@RequestParam("state") String state,
@RequestParam("oauth_token") String requestToken,
@RequestParam("oauth_verifier") String verifier,
Authentication yourLocalUserAuthentication,
HttpServletResponse response) throws IOException {
String userName = yourLocalUserAuthentication.getName();
OAuthConsumerToken latestRequestTokenFromRepository = requestTokenRepository.get(userName);
if (!isAuthorizedState(state)) {
handleAccessConfirmationError(state, response);
return;
}
if (!latestRequestTokenExistsAndMatchesTokenFromRequest(requestToken, latestRequestTokenFromRepository)) {
handleInvalidRequestToken(response);
return;
}
OAuthConsumerToken accessToken = oAuthConsumerSupport.getAccessToken(is24ClientKeyDetails, latestRequestTokenFromRepository, verifier);
accessTokenRepository.put(userName, accessToken);
redirectUserToPageOfYourChoice(response);
}
@GetMapping(value = "/load-real-estates", produces = "text/plain")
public String loadRealEstates(Authentication yourLocalUserAuthentication, HttpServletResponse response) throws IOException, URISyntaxException {
String userName = yourLocalUserAuthentication.getName();
OAuthConsumerToken accessToken = accessTokenRepository.get(userName);
if (accessToken == null) {
response.sendRedirect("/initialize-token-exchange");
return "Redirect";
}
URL url = new URL(RESOURCE_ENDPOINT_URL);
String authHeader = oAuthConsumerSupport.getAuthorizationHeader(is24ClientKeyDetails, accessToken, url, "GET", null);
return webClient.get()
.uri(url.toURI())
.header("Authorization", authHeader)
.retrieve()
.bodyToMono(String.class)
.block();
}
private boolean latestRequestTokenExistsAndMatchesTokenFromRequest(@RequestParam("oauth_token") String requestToken, OAuthConsumerToken latestRequestToken) {
return latestRequestToken != null && latestRequestToken.getValue().equals(requestToken);
}
private void handleInvalidRequestToken(HttpServletResponse response) throws IOException {
response.sendError(HttpStatus.BAD_REQUEST.value(), "Request token for current user does not match token from request!");
}
private void handleAccessConfirmationError(String state, HttpServletResponse response) throws IOException {
if (REJECTED.equals(state)) {
response.sendError(HttpStatus.UNAUTHORIZED.value(), "The token was explicit not authorized by the user!");
} else {
response.sendError(HttpStatus.INTERNAL_SERVER_ERROR.value(), "An error has occurred during authorization!");
}
}
private boolean isAuthorizedState(@RequestParam("state") String state) {
return AUTHORIZED.equals(state);
}
private void redirectUserToPageOfYourChoice(HttpServletResponse response) throws IOException {
response.sendRedirect("/load-real-estates");
}
}
@SpringBootApplication
public class ThreeLeggedOAuth1FlowApplication {
public static void main(String[] args) {
SpringApplication.run(ThreeLeggedOAuth1FlowApplication.class, args);
}
}