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