44
55namespace SimpleSAML \Module \oidc \Controllers \OAuth2 ;
66
7+ use SimpleSAML \Module \oidc \Bridges \OAuth2Bridge ;
78use SimpleSAML \Module \oidc \Codebooks \ApiScopesEnum ;
89use SimpleSAML \Module \oidc \Exceptions \AuthorizationException ;
910use SimpleSAML \Module \oidc \ModuleConfig ;
11+ use SimpleSAML \Module \oidc \Repositories \RefreshTokenRepository ;
1012use SimpleSAML \Module \oidc \Server \Exceptions \OidcServerException ;
1113use SimpleSAML \Module \oidc \Server \Validators \BearerTokenValidator ;
1214use SimpleSAML \Module \oidc \Services \Api \Authorization ;
@@ -34,6 +36,8 @@ public function __construct(
3436 protected readonly Authorization $ apiAuthorization ,
3537 protected readonly RequestParamsResolver $ requestParamsResolver ,
3638 protected readonly BearerTokenValidator $ bearerTokenValidator ,
39+ protected readonly OAuth2Bridge $ oAuth2Bridge ,
40+ protected readonly RefreshTokenRepository $ refreshTokenRepository ,
3741 ) {
3842 if (!$ this ->moduleConfig ->getApiEnabled ()) {
3943 $ this ->loggerService ->warning ('API capabilities not enabled. ' );
@@ -48,7 +52,6 @@ public function __construct(
4852
4953 public function __invoke (Request $ request ): Response
5054 {
51- // TODO mivanci Add support for Refresh Tokens.
5255 // TODO mivanci Add endpoint to OAuth2 discovery document.
5356
5457 try {
@@ -80,46 +83,140 @@ public function __invoke(Request $request): Response
8083 );
8184 }
8285
83- // For now, we will only support Access Tokens.
84- // $tokenTypeHintParam = $this->requestParamsResolver->getFromRequestBasedOnAllowedMethods(
85- // ParamsEnum::TokenTypeHint->value,
86- // $request,
87- // $allowedMethods,
88- // );
86+ $ tokenTypeHintParam = $ this ->requestParamsResolver ->getFromRequestBasedOnAllowedMethods (
87+ ParamsEnum::TokenTypeHint->value ,
88+ $ request ,
89+ $ allowedMethods ,
90+ );
8991
92+ $ payload = null ;
93+ if (is_null ($ tokenTypeHintParam )) {
94+ $ payload = $ this ->resolveAccessTokenPayload ($ tokenParam ) ??
95+ $ this ->resolveRefreshTokenPayload ($ tokenParam );
96+ } elseif ($ tokenTypeHintParam === 'access_token ' ) {
97+ $ payload = $ this ->resolveAccessTokenPayload ($ tokenParam );
98+ } elseif ($ tokenTypeHintParam === 'refresh_token ' ) {
99+ $ payload = $ this ->resolveRefreshTokenPayload ($ tokenParam );
100+ }
101+
102+ $ payload = $ payload ?? ['active ' => false ];
103+
104+ return $ this ->routes ->newJsonResponse ($ payload );
105+ }
106+
107+ protected function resolveAccessTokenPayload (string $ tokenParam ): ?array
108+ {
90109 try {
91110 $ accessToken = $ this ->bearerTokenValidator ->ensureValidAccessToken ($ tokenParam );
92111 } catch (\Throwable $ e ) {
93- $ this ->loggerService ->error ('Token validation failed: ' . $ e ->getMessage ());
94- return $ this -> routes -> newJsonResponse ([ ' active ' => false ]) ;
112+ $ this ->loggerService ->error ('Access token validation failed: ' . $ e ->getMessage ());
113+ return null ;
95114 }
115+
116+ // See \SimpleSAML\Module\oidc\Entities\AccessTokenEntity::convertToJWT
117+ // for claims set on the access token.
118+
96119 $ scopeClaim = null ;
97120 /** @psalm-suppress MixedAssignment */
98121 $ accessTokenScopes = $ accessToken ->getPayloadClaim ('scopes ' );
99122 if (is_array ($ accessTokenScopes )) {
100- $ accessTokenScopes = array_filter (
101- $ accessTokenScopes ,
102- static fn ($ scope ) => is_string ($ scope ) && !empty ($ scope ),
103- );
104- $ scopeClaim = implode (' ' , $ accessTokenScopes );
123+ $ scopeClaim = $ this ->prepareScopeString ($ accessTokenScopes );
105124 }
106125
107- $ audience = is_array ($ audience = $ accessToken ->getAudience ()) ? $ audience [0 ] ?? null : null ;
126+ $ clientId = is_array ($ audience = $ accessToken ->getAudience ()) ? $ audience [0 ] ?? null : null ;
108127
109- $ payload = array_filter ([
128+ return array_filter ([
110129 'active ' => true ,
111130 'scope ' => $ scopeClaim ,
131+ 'client_id ' => $ clientId ,
112132 'token_type ' => 'Bearer ' ,
113133 ClaimsEnum::Exp->value => $ accessToken ->getExpirationTime (),
114134 ClaimsEnum::Iat->value => $ accessToken ->getIssuedAt (),
115135 ClaimsEnum::Nbf->value => $ accessToken ->getNotBefore (),
116136 ClaimsEnum::Sub->value => $ accessToken ->getSubject (),
117- ClaimsEnum::Aud->value => $ audience ,
137+ ClaimsEnum::Aud->value => $ accessToken -> getAudience () ,
118138 ClaimsEnum::Iss->value => $ accessToken ->getIssuer (),
119139 ClaimsEnum::Jti->value => $ accessToken ->getJwtId (),
120140 ]);
141+ }
121142
122- return $ this ->routes ->newJsonResponse ($ payload );
143+ /**
144+ * @psalm-suppress MixedAssignment
145+ */
146+ public function resolveRefreshTokenPayload (string $ tokenParam ): ?array
147+ {
148+ try {
149+ $ decryptedToken = $ this ->oAuth2Bridge ->decrypt ($ tokenParam );
150+ $ tokenData = json_decode ($ decryptedToken , true , 512 , JSON_THROW_ON_ERROR );
151+ } catch (\Exception $ e ) {
152+ $ this ->loggerService ->error ('Refresh token decrypting failed: ' . $ e ->getMessage ());
153+ return null ;
154+ }
155+
156+ if (!is_array ($ tokenData )) {
157+ $ this ->loggerService ->error ('Refresh token has unexpected type. ' );
158+ return null ;
159+ }
160+
161+ // See \League\OAuth2\Server\ResponseTypes\BearerTokenResponse::generateHttpResponse for claims set on
162+ // the refresh token.
163+
164+ $ expireTime = is_int ($ expireTime = $ tokenData ['expire_time ' ] ?? null ) ? $ expireTime : null ;
165+
166+ if (is_null ($ expireTime )) {
167+ $ this ->loggerService ->error ('Refresh token has no expiration time. ' );
168+ return null ;
169+ }
170+
171+ if ($ expireTime < time ()) {
172+ $ this ->loggerService ->error ('Refresh token has expired. ' );
173+ return null ;
174+ }
175+
176+ $ refreshTokenId = is_string ($ refreshTokenId = $ tokenData ['refresh_token_id ' ] ?? null ) ? $ refreshTokenId : null ;
177+
178+ if (is_null ($ refreshTokenId )) {
179+ $ this ->loggerService ->error ('Refresh token has no ID. ' );
180+ return null ;
181+ }
182+
183+ try {
184+ if ($ this ->refreshTokenRepository ->isRefreshTokenRevoked ($ refreshTokenId )) {
185+ $ this ->loggerService ->error ('Refresh token has been revoked. ' );
186+ return null ;
187+ }
188+ } catch (OidcServerException $ e ) {
189+ $ this ->loggerService ->error ('Refresh token revocation check failed: ' . $ e ->getMessage ());
190+ return null ;
191+ }
192+
193+ $ scopeClaim = null ;
194+ $ refreshTokenScopes = $ tokenData ['scopes ' ] ?? null ;
195+ if (is_array ($ refreshTokenScopes )) {
196+ $ scopeClaim = $ this ->prepareScopeString ($ refreshTokenScopes );
197+ }
198+
199+ $ clientId = is_string ($ clientId = $ tokenData ['client_id ' ] ?? null ) ? $ clientId : null ;
200+
201+ return array_filter ([
202+ 'active ' => true ,
203+ 'scope ' => $ scopeClaim ,
204+ 'client_id ' => $ clientId ,
205+ ClaimsEnum::Exp->value => $ expireTime ,
206+ ClaimsEnum::Sub->value => is_string ($ tokenData ['user_id ' ] ?? null ) ? $ tokenData ['user_id ' ] : null ,
207+ ClaimsEnum::Aud->value => $ clientId ,
208+ ClaimsEnum::Jti->value => $ refreshTokenId ,
209+ ]);
210+ }
211+
212+ protected function prepareScopeString (array $ scopes ): string
213+ {
214+ $ scopes = array_filter (
215+ $ scopes ,
216+ static fn ($ scope ) => is_string ($ scope ) && !empty ($ scope ),
217+ );
218+
219+ return implode (' ' , $ scopes );
123220 }
124221
125222 /**
@@ -138,19 +235,24 @@ protected function ensureAuthenticatedClient(Request $request): void
138235 $ resolvedClientAuthenticationMethod ->getClientAuthenticationMethod ()->isNotNone ()
139236 ) {
140237 $ this ->loggerService ->debug (
141- 'Client authenticated using supported OAuth2 client authentication method: ' .
142- $ resolvedClientAuthenticationMethod ->getClientAuthenticationMethod ()->value ,
238+ sprintf (
239+ 'Client %s authenticated using supported OAuth2 client authentication method %s. ' ,
240+ $ resolvedClientAuthenticationMethod ->getClient ()->getIdentifier (),
241+ $ resolvedClientAuthenticationMethod ->getClientAuthenticationMethod ()->value ,
242+ ),
143243 );
244+
144245 return ;
145246 }
146247
147248 $ this ->loggerService ->debug ('No regular OAuth2 client authentication method found. ' );
148249 $ this ->loggerService ->debug ('Trying API client authentication method. ' );
149250
150-
151251 $ this ->apiAuthorization ->requireTokenForAnyOfScope (
152252 $ request ,
153253 [ApiScopesEnum::OAuth2TokenIntrospection, ApiScopesEnum::OAuth2All, ApiScopesEnum::All],
154254 );
255+
256+ $ this ->loggerService ->debug ('API client authenticated. ' );
155257 }
156258}
0 commit comments