PocketMine-MP 5.35.1 git-e32e836dad793a3a3c8ddd8927c00e112b1e576a
Loading...
Searching...
No Matches
AuthJwtHelper.php
1<?php
2
3/*
4 *
5 * ____ _ _ __ __ _ __ __ ____
6 * | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
7 * | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
8 * | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
9 * |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
10 *
11 * This program is free software: you can redistribute it and/or modify
12 * it under the terms of the GNU Lesser General Public License as published by
13 * the Free Software Foundation, either version 3 of the License, or
14 * (at your option) any later version.
15 *
16 * @author PocketMine Team
17 * @link http://www.pocketmine.net/
18 *
19 *
20 */
21
22declare(strict_types=1);
23
24namespace pocketmine\network\mcpe\auth;
25
33use function base64_decode;
34use function time;
35
36final class AuthJwtHelper{
37
38 public const MOJANG_AUDIENCE = "api://auth-minecraft-services/multiplayer";
39
40 private const CLOCK_DRIFT_MAX = 60;
41
45 private static function checkExpiry(JwtBodyRfc7519 $claims) : void{
46 $time = time();
47 if(isset($claims->nbf) && $claims->nbf > $time + self::CLOCK_DRIFT_MAX){
48 throw new VerifyLoginException("JWT not yet valid", KnownTranslationFactory::pocketmine_disconnect_invalidSession_tooEarly());
49 }
50
51 if(isset($claims->exp) && $claims->exp < $time - self::CLOCK_DRIFT_MAX){
52 throw new VerifyLoginException("JWT expired", KnownTranslationFactory::pocketmine_disconnect_invalidSession_tooLate());
53 }
54 }
55
59 public static function validateOpenIdAuthToken(string $jwt, string $signingKeyDer, string $issuer, string $audience) : XboxAuthJwtBody{
60 try{
61 if(!JwtUtils::verify($jwt, $signingKeyDer, ec: false)){
62 throw new VerifyLoginException("Invalid JWT signature", KnownTranslationFactory::pocketmine_disconnect_invalidSession_badSignature());
63 }
64 }catch(JwtException $e){
65 throw new VerifyLoginException($e->getMessage(), null, 0, $e);
66 }
67
68 try{
69 [, $claimsArray, ] = JwtUtils::parse($jwt);
70 }catch(JwtException $e){
71 throw new VerifyLoginException("Failed to parse JWT: " . $e->getMessage(), null, 0, $e);
72 }
73
74 $mapper = new \JsonMapper();
75 $mapper->bExceptionOnUndefinedProperty = false; //we only care about the properties we're using in this case
76 $mapper->bExceptionOnMissingData = true;
77 $mapper->bStrictObjectTypeChecking = true;
78 $mapper->bEnforceMapType = false;
79 $mapper->bRemoveUndefinedAttributes = true;
80
81 try{
82 //nasty dynamic new for JsonMapper
83 $claims = $mapper->map($claimsArray, new XboxAuthJwtBody());
84 }catch(\JsonMapper_Exception $e){
85 throw new VerifyLoginException("Invalid chain link body: " . $e->getMessage(), null, 0, $e);
86 }
87
88 if(!isset($claims->iss) || $claims->iss !== $issuer){
89 throw new VerifyLoginException("Invalid JWT issuer");
90 }
91
92 if(!isset($claims->aud) || $claims->aud !== $audience){
93 throw new VerifyLoginException("Invalid JWT audience");
94 }
95
96 self::checkExpiry($claims);
97
98 return $claims;
99 }
100
104 public static function validateLegacyAuthToken(string $jwt, ?string $expectedKeyDer) : LegacyAuthJwtBody{
105 self::validateSelfSignedToken($jwt, $expectedKeyDer);
106
107 //TODO: this parses the JWT twice and throws away a bunch of parts, optimize this
108 [, $claimsArray, ] = JwtUtils::parse($jwt);
109
110 $mapper = new \JsonMapper();
111 $mapper->bExceptionOnUndefinedProperty = false; //we only care about the properties we're using in this case
112 $mapper->bExceptionOnMissingData = true;
113 $mapper->bStrictObjectTypeChecking = true;
114 $mapper->bEnforceMapType = false;
115 $mapper->bRemoveUndefinedAttributes = true;
116 try{
118 $claims = $mapper->map($claimsArray, new LegacyAuthJwtBody());
119 }catch(\JsonMapper_Exception $e){
120 throw new VerifyLoginException("Invalid chain link body: " . $e->getMessage(), null, 0, $e);
121 }
122
123 self::checkExpiry($claims);
124
125 return $claims;
126 }
127
128 public static function validateSelfSignedToken(string $jwt, ?string $expectedKeyDer) : void{
129 try{
130 [$headersArray, ] = JwtUtils::parse($jwt);
131 }catch(JwtException $e){
132 throw new VerifyLoginException("Failed to parse JWT: " . $e->getMessage(), null, 0, $e);
133 }
134
135 $mapper = new \JsonMapper();
136 $mapper->bExceptionOnMissingData = true;
137 $mapper->bExceptionOnUndefinedProperty = true;
138 $mapper->bStrictObjectTypeChecking = true;
139 $mapper->bEnforceMapType = false;
140
141 try{
143 $headers = $mapper->map($headersArray, new SelfSignedJwtHeader());
144 }catch(\JsonMapper_Exception $e){
145 throw new VerifyLoginException("Invalid JWT header: " . $e->getMessage(), null, 0, $e);
146 }
147
148 $headerDerKey = base64_decode($headers->x5u, true);
149 if($headerDerKey === false){
150 throw new VerifyLoginException("Invalid JWT public key: base64 decoding error decoding x5u");
151 }
152 if($expectedKeyDer !== null && $headerDerKey !== $expectedKeyDer){
153 //Fast path: if the header key doesn't match what we expected, the signature isn't going to validate anyway
154 throw new VerifyLoginException("Invalid JWT signature", KnownTranslationFactory::pocketmine_disconnect_invalidSession_badSignature());
155 }
156
157 try{
158 if(!JwtUtils::verify($jwt, $headerDerKey, ec: true)){
159 throw new VerifyLoginException("Invalid JWT signature", KnownTranslationFactory::pocketmine_disconnect_invalidSession_badSignature());
160 }
161 }catch(JwtException $e){
162 throw new VerifyLoginException($e->getMessage(), null, 0, $e);
163 }
164 }
165}
static parse(string $token)
Definition JwtUtils.php:96
static validateLegacyAuthToken(string $jwt, ?string $expectedKeyDer)
static validateOpenIdAuthToken(string $jwt, string $signingKeyDer, string $issuer, string $audience)