From cf86629f4d1f4a70207a5e5ac129c9d61e4ba4cb Mon Sep 17 00:00:00 2001 From: gsd Date: Mon, 6 Feb 2023 21:54:07 +0300 Subject: [PATCH] steam auth --- .../java/app/controllers/AuthController.java | 64 +++++++++ src/main/java/app/services/SteamSignIn.java | 123 ++++++++++++++++++ src/main/java/app/utils/SaltedCookie.java | 23 ++++ src/main/resources/application.yaml | 7 +- 4 files changed, 216 insertions(+), 1 deletion(-) create mode 100644 src/main/java/app/controllers/AuthController.java create mode 100644 src/main/java/app/services/SteamSignIn.java create mode 100644 src/main/java/app/utils/SaltedCookie.java diff --git a/src/main/java/app/controllers/AuthController.java b/src/main/java/app/controllers/AuthController.java new file mode 100644 index 0000000..dc874fc --- /dev/null +++ b/src/main/java/app/controllers/AuthController.java @@ -0,0 +1,64 @@ +package app.controllers; + +import app.services.SteamSignIn; +import app.utils.SaltedCookie; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import java.util.Map; + +@RestController +@RequestMapping("/api/auth") +public class AuthController { + private SteamSignIn steamSignIn; + private SaltedCookie saltedCookie; + + @Autowired + public AuthController(SteamSignIn steamSignIn, SaltedCookie saltedCookie){ + this.steamSignIn = steamSignIn; + this.saltedCookie = saltedCookie; + } + + @GetMapping("login") + public ResponseEntity Login(){ + return steamSignIn.ConstructURLAndRedirect(); + } + + @GetMapping("logout") + public ResponseEntity Logout(HttpServletResponse response){ + Cookie cookie_steam64 = new Cookie("steam64",""); + cookie_steam64.setMaxAge(0); + cookie_steam64.setPath("/"); + response.addCookie(cookie_steam64); + Cookie cookie_steam64_secured = new Cookie("steam64_secured", ""); + cookie_steam64_secured.setMaxAge(0); + cookie_steam64.setPath("/"); + response.addCookie(cookie_steam64_secured); + return ResponseEntity.ok().body("logout..."); + } + + @GetMapping("processlogin") + public ResponseEntity ProcessLogin(@RequestParam Map auth_result, HttpServletResponse response){ + System.out.println(auth_result); + Long steam64 = steamSignIn.ValidateResults(auth_result); + if(steam64 == null){ + return new ResponseEntity<>("returned steam is not valid",HttpStatus.FORBIDDEN); + } + + Cookie cookie_steam64 = new Cookie("steam64", steam64.toString()); + cookie_steam64.setPath("/"); + response.addCookie(cookie_steam64); + Cookie cookie_steam64_secured = new Cookie("steam64_secured", saltedCookie.Hashed(steam64.toString())); + cookie_steam64_secured.setPath("/"); + response.addCookie(cookie_steam64_secured); + + return ResponseEntity.ok() + .body("login successful"); + } +} diff --git a/src/main/java/app/services/SteamSignIn.java b/src/main/java/app/services/SteamSignIn.java new file mode 100644 index 0000000..bf0c534 --- /dev/null +++ b/src/main/java/app/services/SteamSignIn.java @@ -0,0 +1,123 @@ +package app.services; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.*; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.DefaultUriBuilderFactory; +import org.springframework.web.util.UriBuilder; + +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URLDecoder; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +@Service +public class SteamSignIn { + private final String provider = "https://steamcommunity.com/openid/login?"; + + @Value("${backend.auth.response_redirect}") + private String responseURL; + + private RestTemplate restTemplate; + + private final Pattern refinedScripts = Pattern.compile("(?:http)"); + private final Pattern validAuth = Pattern.compile("is_valid:true"); + private final Pattern Check64ID = Pattern.compile("https://steamcommunity.com/openid/id/(\\d+)"); + + @Autowired + public void SteamSignIn(){ + this.restTemplate = new RestTemplate(); + + //http://www.chrispad.com/2019/04/disable-encoding-url-using-resttemplate.html + //https://stackoverflow.com/questions/66164546/resttemplate-exchange-fail-on-get-call-but-works-on-curl + //disable double encoding with rest + DefaultUriBuilderFactory defaultUriBuilderFactory = new DefaultUriBuilderFactory(); + defaultUriBuilderFactory.setEncodingMode(DefaultUriBuilderFactory.EncodingMode.NONE); + this.restTemplate.setUriTemplateHandler(defaultUriBuilderFactory); + } + + private String encodeValue(String value){ + try { + return URLEncoder.encode(value, StandardCharsets.UTF_8.toString()); + } catch (UnsupportedEncodingException err){ + return ""; + } + } + + private String decodeValue(String value) { + try { + return URLDecoder.decode(value, StandardCharsets.UTF_8.toString()); + } catch (UnsupportedEncodingException err){ + return ""; + } + } + + public ResponseEntity ConstructURLAndRedirect(){ + Matcher result = refinedScripts.matcher(responseURL); + if (!result.find() || result.group(0).isEmpty()){ + responseURL = String.format("http://%s", responseURL); + } + + HashMap authParameters = new HashMap<>(); + authParameters.put("openid.ns", "http://specs.openid.net/auth/2.0"); + authParameters.put("openid.mode", "checkid_setup"); + authParameters.put("openid.return_to", responseURL); + authParameters.put("openid.realm", responseURL); + authParameters.put("openid.identity", "http://specs.openid.net/auth/2.0/identifier_select"); + authParameters.put("openid.claimed_id", "http://specs.openid.net/auth/2.0/identifier_select"); + + String url = authParameters.keySet().stream() + .map(key -> key + "=" + encodeValue(authParameters.get(key))) + .collect(Collectors.joining("&", provider, "")); + + return ResponseEntity.status(HttpStatus.SEE_OTHER). + header("Content-Type", "application/x-www-form-urlencoded"). + location(URI.create(url)) + .build(); + } + + //@RequestParam Map reqParam + public Long ValidateResults(Map results){ + Map validationArgs = new HashMap<>(); + validationArgs.put("openid.assoc_handle", results.get("openid.assoc_handle")); + validationArgs.put("openid.signed", results.get("openid.signed")); + validationArgs.put("openid.sig", results.get("openid.sig")); + validationArgs.put("openid.ns", results.get("openid.ns")); + + List signedArgs = Arrays.stream(results.get("openid.signed").split(",")).toList(); + for(String item: signedArgs){ + String itemArg = String.format("openid.%s", item); + if (!validationArgs.containsKey(results.get(itemArg))) { + validationArgs.put(itemArg, results.get(itemArg)); + } + } + validationArgs.put("openid.mode", "check_authentication"); + + String url = validationArgs.keySet().stream() + .map(key -> key + "=" + encodeValue(validationArgs.get(key))) + .collect(Collectors.joining("&", provider, "")); + + String responseData = restTemplate.getForObject(url, String.class); + + if (validAuth.matcher(responseData).find()) { + Matcher matcher = Check64ID.matcher(results.get("openid.claimed_id")); + if (!matcher.find() || !matcher.group(1).isEmpty()) { + return Long.valueOf(matcher.group(1)); + } else { + return null; + } + } else { + return null; + } + } +} diff --git a/src/main/java/app/utils/SaltedCookie.java b/src/main/java/app/utils/SaltedCookie.java new file mode 100644 index 0000000..46d4859 --- /dev/null +++ b/src/main/java/app/utils/SaltedCookie.java @@ -0,0 +1,23 @@ +package app.utils; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.util.DigestUtils; + +@Component +public class SaltedCookie { + @Value("${backend.auth.salt}") + private String salt; + + public String Hashed(String value) { + return DigestUtils.md5DigestAsHex(String.format("%s+%s", value, salt).getBytes()); + } + + public boolean Validate(String value, String hashed_value) { + return Hashed(value).equals(hashed_value); + } + + public boolean Validate(Long value, String hashed_value) { + return Validate(value.toString(), hashed_value); + } +} diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index bd46c09..b0b9094 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -29,7 +29,12 @@ backend: unique_server: false ban_count: false a2s: false - countries: true + countries: false + auth: + salt: ${AUTH_SALT} + steam_api_key: ${STEAM_WEBAPI_KEY} + response_redirect: ${AUTH_REDIRECT} + logging: level: com.ibasco.agql.core.util.*: OFF \ No newline at end of file