blob: 62c1efabde389012dd467a8a9b48d2bde40f3970 [file] [log] [blame]
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001<?php
2
3namespace OAuth2\GrantType;
4
5use OAuth2\ClientAssertionType\ClientAssertionTypeInterface;
6use OAuth2\Storage\JwtBearerInterface;
7use OAuth2\Encryption\Jwt;
8use OAuth2\Encryption\EncryptionInterface;
9use OAuth2\ResponseType\AccessTokenInterface;
10use OAuth2\RequestInterface;
11use OAuth2\ResponseInterface;
12
13/**
14 * The JWT bearer authorization grant implements JWT (JSON Web Tokens) as a grant type per the IETF draft.
15 *
16 * @see http://tools.ietf.org/html/draft-ietf-oauth-jwt-bearer-04#section-4
17 *
18 * @author F21
19 * @author Brent Shaffer <bshafs at gmail dot com>
20 */
21class JwtBearer implements GrantTypeInterface, ClientAssertionTypeInterface
22{
23 private $jwt;
24
25 protected $storage;
26 protected $audience;
27 protected $jwtUtil;
28 protected $allowedAlgorithms;
29
30 /**
31 * Creates an instance of the JWT bearer grant type.
32 *
33 * @param JwtBearerInterface $storage - A valid storage interface that implements storage hooks for the JWT
34 * bearer grant type.
35 * @param string $audience - The audience to validate the token against. This is usually the full
36 * URI of the OAuth token requests endpoint.
37 * @param EncryptionInterface|JWT $jwtUtil - OPTONAL The class used to decode, encode and verify JWTs.
38 * @param array $config
39 */
40 public function __construct(JwtBearerInterface $storage, $audience, EncryptionInterface $jwtUtil = null, array $config = array())
41 {
42 $this->storage = $storage;
43 $this->audience = $audience;
44
45 if (is_null($jwtUtil)) {
46 $jwtUtil = new Jwt();
47 }
48
49 $this->config = array_merge(array(
50 'allowed_algorithms' => array('RS256', 'RS384', 'RS512')
51 ), $config);
52
53 $this->jwtUtil = $jwtUtil;
54
55 $this->allowedAlgorithms = $this->config['allowed_algorithms'];
56 }
57
58 /**
59 * Returns the grant_type get parameter to identify the grant type request as JWT bearer authorization grant.
60 *
61 * @return string - The string identifier for grant_type.
62 *
63 * @see GrantTypeInterface::getQueryStringIdentifier()
64 */
65 public function getQueryStringIdentifier()
66 {
67 return 'urn:ietf:params:oauth:grant-type:jwt-bearer';
68 }
69
70 /**
71 * Validates the data from the decoded JWT.
72 *
73 * @param RequestInterface $request
74 * @param ResponseInterface $response
75 * @return bool|mixed|null TRUE if the JWT request is valid and can be decoded. Otherwise, FALSE is returned.@see GrantTypeInterface::getTokenData()
76 */
77 public function validateRequest(RequestInterface $request, ResponseInterface $response)
78 {
79 if (!$request->request("assertion")) {
80 $response->setError(400, 'invalid_request', 'Missing parameters: "assertion" required');
81
82 return null;
83 }
84
85 // Store the undecoded JWT for later use
86 $undecodedJWT = $request->request('assertion');
87
88 // Decode the JWT
89 $jwt = $this->jwtUtil->decode($request->request('assertion'), null, false);
90
91 if (!$jwt) {
92 $response->setError(400, 'invalid_request', "JWT is malformed");
93
94 return null;
95 }
96
97 // ensure these properties contain a value
98 // @todo: throw malformed error for missing properties
99 $jwt = array_merge(array(
100 'scope' => null,
101 'iss' => null,
102 'sub' => null,
103 'aud' => null,
104 'exp' => null,
105 'nbf' => null,
106 'iat' => null,
107 'jti' => null,
108 'typ' => null,
109 ), $jwt);
110
111 if (!isset($jwt['iss'])) {
112 $response->setError(400, 'invalid_grant', "Invalid issuer (iss) provided");
113
114 return null;
115 }
116
117 if (!isset($jwt['sub'])) {
118 $response->setError(400, 'invalid_grant', "Invalid subject (sub) provided");
119
120 return null;
121 }
122
123 if (!isset($jwt['exp'])) {
124 $response->setError(400, 'invalid_grant', "Expiration (exp) time must be present");
125
126 return null;
127 }
128
129 // Check expiration
130 if (ctype_digit($jwt['exp'])) {
131 if ($jwt['exp'] <= time()) {
132 $response->setError(400, 'invalid_grant', "JWT has expired");
133
134 return null;
135 }
136 } else {
137 $response->setError(400, 'invalid_grant', "Expiration (exp) time must be a unix time stamp");
138
139 return null;
140 }
141
142 // Check the not before time
143 if ($notBefore = $jwt['nbf']) {
144 if (ctype_digit($notBefore)) {
145 if ($notBefore > time()) {
146 $response->setError(400, 'invalid_grant', "JWT cannot be used before the Not Before (nbf) time");
147
148 return null;
149 }
150 } else {
151 $response->setError(400, 'invalid_grant', "Not Before (nbf) time must be a unix time stamp");
152
153 return null;
154 }
155 }
156
157 // Check the audience if required to match
158 if (!isset($jwt['aud']) || ($jwt['aud'] != $this->audience)) {
159 $response->setError(400, 'invalid_grant', "Invalid audience (aud)");
160
161 return null;
162 }
163
164 // Check the jti (nonce)
165 // @see http://tools.ietf.org/html/draft-ietf-oauth-json-web-token-13#section-4.1.7
166 if (isset($jwt['jti'])) {
167 $jti = $this->storage->getJti($jwt['iss'], $jwt['sub'], $jwt['aud'], $jwt['exp'], $jwt['jti']);
168
169 //Reject if jti is used and jwt is still valid (exp parameter has not expired).
170 if ($jti && $jti['expires'] > time()) {
171 $response->setError(400, 'invalid_grant', "JSON Token Identifier (jti) has already been used");
172
173 return null;
174 } else {
175 $this->storage->setJti($jwt['iss'], $jwt['sub'], $jwt['aud'], $jwt['exp'], $jwt['jti']);
176 }
177 }
178
179 // Get the iss's public key
180 // @see http://tools.ietf.org/html/draft-ietf-oauth-json-web-token-06#section-4.1.1
181 if (!$key = $this->storage->getClientKey($jwt['iss'], $jwt['sub'])) {
182 $response->setError(400, 'invalid_grant', "Invalid issuer (iss) or subject (sub) provided");
183
184 return null;
185 }
186
187 // Verify the JWT
188 if (!$this->jwtUtil->decode($undecodedJWT, $key, $this->allowedAlgorithms)) {
189 $response->setError(400, 'invalid_grant', "JWT failed signature verification");
190
191 return null;
192 }
193
194 $this->jwt = $jwt;
195
196 return true;
197 }
198
199 /**
200 * Get client id
201 *
202 * @return mixed
203 */
204 public function getClientId()
205 {
206 return $this->jwt['iss'];
207 }
208
209 /**
210 * Get user id
211 *
212 * @return mixed
213 */
214 public function getUserId()
215 {
216 return $this->jwt['sub'];
217 }
218
219 /**
220 * Get scope
221 *
222 * @return null
223 */
224 public function getScope()
225 {
226 return null;
227 }
228
229 /**
230 * Creates an access token that is NOT associated with a refresh token.
231 * If a subject (sub) the name of the user/account we are accessing data on behalf of.
232 *
233 * @see GrantTypeInterface::createAccessToken()
234 *
235 * @param AccessTokenInterface $accessToken
236 * @param mixed $client_id - client identifier related to the access token.
237 * @param mixed $user_id - user id associated with the access token
238 * @param string $scope - scopes to be stored in space-separated string.
239 * @return array
240 */
241 public function createAccessToken(AccessTokenInterface $accessToken, $client_id, $user_id, $scope)
242 {
243 $includeRefreshToken = false;
244
245 return $accessToken->createAccessToken($client_id, $user_id, $scope, $includeRefreshToken);
246 }
247}