1<?php 2/** 3 * EGroupware OpenID Connect / OAuth2 server 4 * 5 * @link https://www.egroupware.org 6 * @author Ralf Becker <rb-At-egroupware.org> 7 * @package openid 8 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License 9 * 10 * Based on the following MIT Licensed packages: 11 * @link https://github.com/steverhoades/oauth2-openid-connect-server 12 * @link https://github.com/thephpleague/oauth2-server 13 */ 14 15namespace EGroupware\OpenID; 16 17use DateInterval; 18use Lcobucci\JWT\Parser; 19use Lcobucci\JWT\Signer\Keychain; 20use Lcobucci\JWT\Signer\Rsa\Sha256; 21use Lcobucci\JWT\ValidationData; 22use League\OAuth2\Server\Grant\AbstractGrant; 23use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface; 24use Psr\Http\Message\ServerRequestInterface; 25 26/** 27 * Generate tokens (programatic) for current user 28 */ 29class Token extends AbstractGrant 30{ 31 /** 32 * Current active user 33 * 34 * @var int 35 */ 36 protected $user; 37 38 function __construct() 39 { 40 $this->user = $GLOBALS['egw_info']['user']['account_id']; 41 42 $this->clientRepository = new Repositories\ClientRepository(); 43 $this->accessTokenRepository = new Repositories\AccessTokenRepository(); 44 $this->refreshTokenRepository = new Repositories\RefreshTokenRepository(); 45 $this->authCodeRepository = new Repositories\AuthCodeRepository(); 46 $this->scopeRepository = new Repositories\ScopeRepository(); 47 $this->privateKey = (new Keys)->getPrivateKey(); 48 } 49 50 /** 51 * Find or generate an access-token for current-user and given client 52 * 53 * Returns NULL if user has not authorized client: no valid access- or refresh-token exists 54 * 55 * @param string $clientIdentifier client-identifier 56 * @param string[] $scopeIdentifiers scope-identifiers 57 * @param string $min_lifetime =null min. lifetime for existing token, null: create new token with default TTL 58 * @param boolean $require_refresh_token =true true: require a refresh token to exist (user authorized before), false: do no check refresh-token 59 * @param string $lifetime =null lifetime of new token or null to use client default 60 * @param boolean|array $return_jwt =true true: return JWT, false: return AccessTokenEntity 61 * or array with name => value pairs with extra claims added to the JWT 62 * @return string|AccessTokenEntity access-token or (signed) JWT 63 */ 64 public function accessToken($clientIdentifier, array $scopeIdentifiers, $min_lifetime=null, 65 $require_refresh_token=true, $lifetime=null, $return_jwt=true) 66 { 67 $scopes = array_map(function($id) 68 { 69 return $this->scopeRepository->getScopeEntityByIdentifier($id); 70 }, $scopeIdentifiers); 71 72 $client = $this->clientRepository->getClientEntity($clientIdentifier, null, null, false); 73 74 if (!empty($min_lifetime)) 75 { 76 $token = $this->accessTokenRepository->findToken($client, $this->user, $min_lifetime); 77 } 78 // if no valid token is found 79 if (!isset($token)) 80 { 81 if ($require_refresh_token && !$this->refreshTokenRepository->findToken($client, $this->user, $min_lifetime)) 82 { 83 return NULL; // user has not yes authorized client 84 } 85 // ToDo: do a propper refresh using RefreshTokenGrant->respondToAccessTokenRequest() 86 // for now we just create a new access-token 87 if (empty($lifetime) && empty($lifetime = $client->getAccessTokenTTL())) 88 { 89 $lifetime = Repositories\ClientRepository::getDefaultAccessTokenTTL(); 90 } 91 $ttl = new DateInterval($lifetime); 92 93 $token = $this->issueAccessToken($ttl, $client, $this->user, $scopes); 94 } 95 return $return_jwt === false ? $token : 96 (string)$token->convertToJWT($this->privateKey, is_array($return_jwt) ? $return_jwt : []); 97 } 98 99 /** 100 * Parse and validate a JWT eg. issued by accessToken method 101 * 102 * We only validate expiration date and signature, not that the token is a (stored and not revoked) access-token. 103 * 104 * @param string $jwt 105 * @return ?Token null if token is expired or signature not valid, otherwise the token to e.g. retrive a claim 106 */ 107 public function validateJWT($jwt) 108 { 109 $token = (new Parser())->parse($jwt); 110 111 if ($this->isTokenExpired($token) || $this->isTokenUnverified($token)) 112 { 113 return null; 114 } 115 return $token; 116 } 117 118 /** 119 * Checks whether the token is unverified. 120 * 121 * @param Token $token 122 * 123 * @return bool 124 */ 125 private function isTokenUnverified(\Lcobucci\JWT\Token $token) 126 { 127 $keychain = new Keychain(); 128 129 $privateKey = new Keys(); 130 $key = $keychain->getPrivateKey( 131 $privateKey->getPrivateKey()->getKeyPath(), 132 $privateKey->getPrivateKey()->getPassPhrase() 133 ); 134 135 return $token->verify(new Sha256(), $key->getContent()) === false; 136 } 137 138 /** 139 * Ensure access token hasn't expired. 140 * 141 * @param Token $token 142 * 143 * @return bool 144 */ 145 private function isTokenExpired(\Lcobucci\JWT\Token $token) 146 { 147 $data = new ValidationData(time()); 148 149 return !$token->validate($data); 150 } 151 152 /** 153 * Generate an auth-code for current-user and given client 154 * 155 * @param string $clientIdentifier client-identifier 156 * @param string[] $scopeIdentifiers scope-identifiers 157 * @param string $lifetime =null lifetime of auth-code, null: use default 158 * @return string access-token 159 */ 160 public function authCode($clientIdentifier, array $scopeIdentifiers, $lifetime=null) 161 { 162 $scopes = array_map(function($id) 163 { 164 return $this->scopeRepository->getScopeEntityByIdentifier($id); 165 }, $scopeIdentifiers); 166 167 $client = $this->clientRepository->getClientEntity($clientIdentifier, null, null, false); 168 $ttl = new DateInterval(empty($lifetime) ? $lifetime : Repositories\ClientRepository::getDefaultAuthCodeTTL()); 169 170 $token = $this->issueAuthCode($ttl, $client, $this->user, $client->getRedirectUri(), $scopes); 171 172 return $token->getIdentifier(); 173 } 174 175 /** 176 * Required to extends AbstractGrant 177 * 178 * @return string 179 */ 180 function getIdentifier() 181 { 182 return null; 183 } 184 185 /** 186 * Required to extends AbstractGrant 187 * 188 * @return string 189 */ 190 public function respondToAccessTokenRequest( 191 ServerRequestInterface $request, 192 ResponseTypeInterface $responseType, 193 DateInterval $accessTokenTTL 194 ) 195 { 196 unset($request, $responseType, $accessTokenTTL); 197 } 198} 199