Skip to content

Commit

Permalink
fixes #142 link to steam
Browse files Browse the repository at this point in the history
  • Loading branch information
Brutus5000 committed Nov 4, 2017
1 parent c7bf63b commit 35b8469
Show file tree
Hide file tree
Showing 14 changed files with 313 additions and 22 deletions.
75 changes: 73 additions & 2 deletions src/inttest/java/com/faforever/api/user/UserControllerTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@


import com.faforever.api.AbstractIntegrationTest;
import com.faforever.api.config.FafApiProperties;
import com.faforever.api.data.domain.User;
import com.faforever.api.email.EmailSender;
import com.faforever.api.error.ErrorCode;
Expand All @@ -21,10 +22,15 @@
import java.time.Duration;

import static junitx.framework.Assert.assertEquals;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.nullValue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl;
Expand All @@ -41,14 +47,17 @@ public class UserControllerTest extends AbstractIntegrationTest {
@MockBean
private EmailSender emailSender;

@MockBean
private SteamService steamService;

@Autowired
private FafTokenService fafTokenService;

@Autowired
private UserService userService;
private UserRepository userRepository;

@Autowired
private UserRepository userRepository;
private FafApiProperties fafApiProperties;

@Test
@WithAnonymousUser
Expand Down Expand Up @@ -188,4 +197,66 @@ public void confirmPasswordReset() throws Exception {
.andExpect(status().isFound())
.andExpect(redirectedUrl("http://localhost/password_resetted"));
}

@Test
@WithAnonymousUser
public void buildSteamLinkUrlUnauthorized() throws Exception {
mockMvc.perform(
post("/users/buildSteamLinkUrl"))
.andExpect(status().isForbidden());
}

@Test
@WithUserDetails(AUTH_USER)
public void buildSteamLinkUrlWithWrongScope() throws Exception {
mockMvc.perform(
post("/users/buildSteamLinkUrl"))
.andExpect(status().isForbidden());
}

@Test
@WithUserDetails(AUTH_MODERATOR)
public void buildSteamLinkUrlAlreadyLinked() throws Exception {
MvcResult result = mockMvc.perform(
post("/users/buildSteamLinkUrl")
.with(getOAuthToken(OAuthScope._WRITE_ACCOUNT_DATA)))
.andExpect(status().is4xxClientError())
.andReturn();

assertApiError(result, ErrorCode.STEAM_ID_UNCHANGEABLE);
}

@Test
@WithUserDetails(AUTH_USER)
public void buildSteamLinkUrl() throws Exception {
when(steamService.buildLoginUrl(any())).thenReturn("steamUrl");

mockMvc.perform(
post("/users/buildSteamLinkUrl")
.with(getOAuthToken(OAuthScope._WRITE_ACCOUNT_DATA)))
.andExpect(status().isOk());

verify(steamService, times(1)).buildLoginUrl(anyString());
}

@Test
@WithAnonymousUser
public void linkToSteam() throws Exception {
assertThat(userRepository.findOne(1).getSteamId(), nullValue());

when(steamService.parseSteamIdFromLoginRedirect(any())).thenReturn("12345");
when(steamService.ownsForgedAlliance(anyString())).thenReturn(true);

String token = fafTokenService.createToken(
FafTokenType.LINK_TO_STEAM,
Duration.ofSeconds(100),
ImmutableMap.of(UserService.KEY_USER_ID, "1"));

mockMvc.perform(
get(String.format("/users/linkToSteam?token=%s&openid.identity=http://steamcommunity.com/openid/id/12345", token)))
.andExpect(status().isFound())
.andExpect(redirectedUrl(fafApiProperties.getLinkToSteam().getSuccessRedirectUrl()));

assertThat(userRepository.findOne(1).getSteamId(), is("12345"));
}
}
7 changes: 7 additions & 0 deletions src/inttest/resources/config/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,10 @@ faf-api:
html-format: "Integration test registration html body"
from-email: "integration-test@faforever.com"
from-name: "integration-test@faforever.com"
link-to-steam:
steam-redirect-url-format: "http://localhost:8010/users/linkToSteam?token=%s"
success-redirect-url: "http://localhost/linked_to_steam"
error-redirect-url-format: "http://localhost/error?message=%s"
steam:
realm: "http://localhost"
api-key: "banana"
15 changes: 7 additions & 8 deletions src/inttest/resources/sql/prepDefaultUser.sql
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,12 @@ DELETE FROM oauth_clients;
DELETE FROM login;

INSERT INTO oauth_clients (id, name, client_secret, client_type, redirect_uris, default_redirect_uri, default_scope)
VALUES ('test', 'test', 'test', 'public', 'http://localhost', 'http://localhost',
'read_events read_achievements upload_map upload_mod write_account_data');
VALUES
('test', 'test', 'test', 'public', 'http://localhost https://www.getpostman.com/oauth2/callback ', 'http://localhost',
'read_events read_achievements upload_map upload_mod write_account_data');

INSERT INTO login (id, login, email, password)
VALUES (1, 'USER', 'user@faforever.com', '92b7b421992ef490f3b75898ec0e511f1a5c02422819d89719b20362b023ee4f');
INSERT INTO login (id, login, email, password)
VALUES (2, 'MODERATOR', 'moderator@faforever.com', '778ac5b81fa251b450f827846378739caee510c31b01cfa9d31822b88bed8441');
INSERT INTO login (id, login, email, password)
VALUES (3, 'ADMIN', 'admin@faforever.com', '835d6dc88b708bc646d6db82c853ef4182fabbd4a8de59c213f2b5ab3ae7d9be');
INSERT INTO login (id, login, email, password, steamid) VALUES
(1, 'USER', 'user@faforever.com', '92b7b421992ef490f3b75898ec0e511f1a5c02422819d89719b20362b023ee4f', NULL),
(2, 'MODERATOR', 'moderator@faforever.com', '778ac5b81fa251b450f827846378739caee510c31b01cfa9d31822b88bed8441', 1234),
(3, 'ADMIN', 'admin@faforever.com', '835d6dc88b708bc646d6db82c853ef4182fabbd4a8de59c213f2b5ab3ae7d9be', NULL);

18 changes: 18 additions & 0 deletions src/main/java/com/faforever/api/config/FafApiProperties.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ public class FafApiProperties {
private Deployment deployment = new Deployment();
private Registration registration = new Registration();
private PasswordReset passwordReset = new PasswordReset();
private LinkToSteam linkToSteam = new LinkToSteam();
private Steam steam = new Steam();
private Mail mail = new Mail();
private Challonge challonge = new Challonge();
private User user = new User();
Expand Down Expand Up @@ -165,6 +167,22 @@ public static class PasswordReset {
private String successRedirectUrl;
}

@Data
public static class LinkToSteam {
private String steamRedirectUrlFormat;
private String successRedirectUrl;
private String errorRedirectUrlFormat;
}

@Data
public static class Steam {
String realm;
String apiKey;
String forgedAllianceAppId = "9420";
String loginUrlFormat = "https://steamcommunity.com/openid/login?%s";
String getOwnedGamesUrlFormat = "https://api.steampowered.com/IPlayerService/GetOwnedGames/v0001?%s";
}

@Data
public static class Challonge {
private String baseUrl = "https://api.challonge.com";
Expand Down
3 changes: 2 additions & 1 deletion src/main/java/com/faforever/api/error/ErrorCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@ public enum ErrorCode {
MOD_VERSION_NOT_A_NUMBER(161, "Mod version is not a number", "The mod version has to be a whole number like 123, but was ''{0}''"),
USERNAME_RESERVED(162, "Invalid account data", "The username ''{0}'' can only be claimed by the original owner within {1} months after it has been freed."),
UNKNOWN_IDENTIFIER(163, "Unable to resolve user", "The identifier does neither match a username nor an email: {0}"),
ALREADY_REGISTERED(164, "Registration failed", "You can't create a new account because you already have one.");
ALREADY_REGISTERED(164, "Registration failed", "You can't create a new account because you already have one."),
STEAM_LINK_NO_FA_GAME(141, "Linking to Steam failed", "You do not own Forged Alliance on Steam or your profile is private.");

private final int code;
private final String title;
Expand Down
8 changes: 4 additions & 4 deletions src/main/java/com/faforever/api/security/FafTokenService.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import java.time.temporal.TemporalAmount;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

@Service
@Slf4j
Expand All @@ -28,12 +29,10 @@ public class FafTokenService {
static final String KEY_LIFETIME = "lifetime";

private final ObjectMapper objectMapper;
private final FafApiProperties properties;
private final MacSigner macSigner;

public FafTokenService(ObjectMapper objectMapper, FafApiProperties properties) {
this.objectMapper = objectMapper;
this.properties = properties;
this.macSigner = new MacSigner(properties.getJwt().getSecret());
}

Expand All @@ -53,7 +52,7 @@ public String createToken(@NotNull FafTokenType type, @NotNull TemporalAmount li
Instant expiresAt = Instant.now().plus(lifetime);
claims.put(KEY_LIFETIME, expiresAt.toString());

log.debug("Creating token of type `{0}` expiring at `{1}` with attributes: {2}", type, expiresAt, attributes);
log.debug("Creating token of type `{}` expiring at `{}` with attributes: {}", type, expiresAt, attributes);

return JwtHelper.encode(objectMapper.writeValueAsString(claims), macSigner).getEncoded();
}
Expand All @@ -74,7 +73,8 @@ public Map<String, String> resolveToken(@NotNull FafTokenType expectedTokenType,
Assert.isTrue(claims.containsKey(KEY_ACTION), "Token does not contain: " + KEY_ACTION);
Assert.isTrue(claims.containsKey(KEY_LIFETIME), "Token does not contain: " + KEY_LIFETIME);
FafTokenType actualTokenType = FafTokenType.valueOf(claims.get(KEY_ACTION));
Assert.state(expectedTokenType == actualTokenType, String.format("Token types do not match, expected: %s, actual: %s", expectedTokenType, actualTokenType));
Assert.state(Objects.equals(expectedTokenType, actualTokenType),
String.format("Token types do not match, expected: %s, actual: %s", expectedTokenType, actualTokenType));
} catch (IOException | IllegalArgumentException | IllegalStateException e) {
if (claims == null) {
log.warn("Unparseable token of expected type {}: {}", expectedTokenType, token);
Expand Down
3 changes: 2 additions & 1 deletion src/main/java/com/faforever/api/security/FafTokenType.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@
@AllArgsConstructor
public enum FafTokenType {
REGISTRATION,
PASSWORD_RESET
PASSWORD_RESET,
LINK_TO_STEAM
}
68 changes: 68 additions & 0 deletions src/main/java/com/faforever/api/user/SteamService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package com.faforever.api.user;

import com.faforever.api.config.FafApiProperties;
import com.faforever.api.config.FafApiProperties.Steam;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.NameValuePair;
import org.apache.http.client.utils.URLEncodedUtils;
import org.apache.http.message.BasicNameValuePair;
import org.json.JSONObject;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.List;

@Service
@Slf4j
public class SteamService {
private final FafApiProperties properties;

public SteamService(FafApiProperties properties) {
this.properties = properties;
}

String buildLoginUrl(String redirectUrl) {
log.trace("Building steam login url for redirect url: {}", redirectUrl);

List<NameValuePair> steamArgs = new ArrayList<>();
steamArgs.add(new BasicNameValuePair("openid.ns", "http://specs.openid.net/auth/2.0"));
steamArgs.add(new BasicNameValuePair("openid.mode", "checkid_setup"));
steamArgs.add(new BasicNameValuePair("openid.return_to", redirectUrl));
steamArgs.add(new BasicNameValuePair("openid.realm", properties.getSteam().getRealm()));
steamArgs.add(new BasicNameValuePair("openid.identity", "http://specs.openid.net/auth/2.0/identifier_select"));
steamArgs.add(new BasicNameValuePair("openid.claimed_id", "http://specs.openid.net/auth/2.0/identifier_select"));
String queryArgs = URLEncodedUtils.format(steamArgs, "utf8");

return String.format(properties.getSteam().getLoginUrlFormat(), queryArgs);
}

String parseSteamIdFromLoginRedirect(HttpServletRequest request) {
log.trace("Parsing steam id from request: {}", request);

String identityUrl = request.getParameter("openid.identity");
return identityUrl.substring(identityUrl.lastIndexOf("/") + 1, identityUrl.length());
}

@SneakyThrows
boolean ownsForgedAlliance(String steamId) {
log.debug("Checking whether steamId owns Forged Alliance: {}", steamId);

Steam steam = properties.getSteam();

RestTemplate restTemplate = new RestTemplate();
List<NameValuePair> steamArgs = new ArrayList<>();
steamArgs.add(new BasicNameValuePair("key", steam.getApiKey()));
steamArgs.add(new BasicNameValuePair("steamid", steamId));
steamArgs.add(new BasicNameValuePair("format", "json"));
steamArgs.add(new BasicNameValuePair("appids_filter[0]", steam.getForgedAllianceAppId()));
String queryArgs = URLEncodedUtils.format(steamArgs, "utf8");

String response = restTemplate.getForObject(String.format(steam.getGetOwnedGamesUrlFormat(), queryArgs), String.class);
JSONObject result = new JSONObject(response);

return result.getJSONObject("response").getInt("game_count") > 0;
}
}
28 changes: 27 additions & 1 deletion src/main/java/com/faforever/api/user/UserController.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.faforever.api.error.ApiException;
import com.faforever.api.error.Error;
import com.faforever.api.error.ErrorCode;
import com.google.common.collect.ImmutableMap;
import io.swagger.annotations.ApiOperation;
import org.springframework.http.MediaType;
import org.springframework.security.access.prepost.PreAuthorize;
Expand All @@ -16,16 +17,20 @@
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.Serializable;
import java.util.Map;

@RestController
@RequestMapping(path = "/users")
public class UserController {
private final FafApiProperties fafApiProperties;
private final UserService userService;
private final SteamService steamService;

public UserController(FafApiProperties fafApiProperties, UserService userService) {
public UserController(FafApiProperties fafApiProperties, UserService userService, SteamService steamService) {
this.fafApiProperties = fafApiProperties;
this.userService = userService;
this.steamService = steamService;
}

@ApiOperation("Registers a new account that needs to be activated.")
Expand Down Expand Up @@ -77,4 +82,25 @@ public void claimPasswordResetToken(HttpServletResponse response,
userService.claimPasswordResetToken(token, newPassword);
response.sendRedirect(fafApiProperties.getPasswordReset().getSuccessRedirectUrl());
}

@PreAuthorize("#oauth2.hasScope('write_account_data') and hasRole('ROLE_USER')")
@ApiOperation("Creates an URL to the steam platform to initiate the Link To Steam process.")
@RequestMapping(path = "/buildSteamLinkUrl", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE)
public Map<String, Serializable> buildSteamLinkUrl(Authentication authentication) {
String steamUrl = userService.buildSteamLinkUrl(userService.getUser(authentication));
return ImmutableMap.of("steam_url", steamUrl);
}

@ApiOperation("Processes the Steam redirect and creates the steam link in the user account.")
@RequestMapping(path = "/linkToSteam", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
public void linkToSteam(HttpServletRequest request,
HttpServletResponse response,
@RequestParam("token") String token) throws IOException {
try {
userService.linkToSteam(token, steamService.parseSteamIdFromLoginRedirect(request));
response.sendRedirect(fafApiProperties.getLinkToSteam().getSuccessRedirectUrl());
} catch (ApiException e) {
response.sendRedirect(String.format(fafApiProperties.getLinkToSteam().getErrorRedirectUrlFormat(), e.getMessage()));
}
}
}
Loading

0 comments on commit 35b8469

Please sign in to comment.