Ticket::decodeTicket()   A
last analyzed

Complexity

Conditions 3
Paths 4

Size

Total Lines 22
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 22
rs 9.2
cc 3
eloc 12
nc 4
nop 1
1
<?php
2
/**
3
 * Implements CAS tickets for the plugin's CAS server.
4
 *
5
 * @version 1.1.0
6
 * @since   1.1.0
7
 */
8
9
namespace Cassava\CAS;
10
11
use Cassava\Exception\TicketException;
12
use Cassava\Options;
13
use Cassava\Plugin;
14
15
/**
16
 * Class that implements CAS tickets.
17
 *
18
 * @version 1.1.0
19
 * @since   1.1.0
20
 */
21
class Ticket {
22
23
	/**
24
	 * Service Ticket
25
	 */
26
	const TYPE_ST = 'ST';
27
28
	/**
29
	 * Proxy Ticket
30
	 */
31
	const TYPE_PT = 'PT';
32
33
	/**
34
	 * Proxy-Granting Ticket
35
	 */
36
	const TYPE_PGT = 'PGT';
37
38
	/**
39
	 * Proxy-Granting Ticket IOU
40
	 */
41
	const TYPE_PGTIOU = 'PGTIOU';
42
43
	/**
44
	 * Ticket-Granting Cookie
45
	 */
46
	const TYPE_TGC = 'TGC';
47
48
	/**
49
	 * Login Ticket
50
	 */
51
	const TYPE_LT = 'LT';
52
53
	/**
54
	 * Ticket type.
55
	 * @var string
56
	 */
57
	public $type;
58
59
	/**
60
	 * Authenticated WordPress user who owns the ticket.
61
	 * @var WP_User
62
	 */
63
	public $user;
64
65
	/**
66
	 * URL for the service that requested authentication.
67
	 * @var string
68
	 */
69
	public $service;
70
71
	/**
72
	 * Expiration timestamp, in seconds.
73
	 * @var float
74
	 */
75
	public $expires;
76
77
	/**
78
	 * CAS ticket constructor.
79
	 *
80
	 * @param string  $type       Ticket type.
81
	 * @param WP_User $user       Authenticated WordPress user who owns the ticket.
82
	 * @param string  $service    URL for the service that requested authentication.
83
	 * @param double  $expires    Expiration timestamp, in seconds.
84
	 *                            Freshly generated tickets should not provide this value.
85
	 *
86
	 * @todo "Remember-Me" tickets should have an expiration date of up to 3 months.
87
	 */
88
	public function __construct( $type, $user, $service, $expires = 0.0 ) {
89
		$this->type    = $type;
90
		$this->user    = $user;
91
		$this->service = \esc_url_raw( $service );
92
		$this->expires = $expires;
93
94
		/**
95
		 * Freshly generated tickets have no expiration timestamp:
96
		 */
97
		if ( ! $expires ) {
98
			$expiration  = Options::get( 'expiration', 30 );
99
100
			/**
101
			 * This filter allows developers to override the default ticket expiration period.
102
			 *
103
			 * @param  int     $expiration Ticket expiration period (in seconds).
104
			 * @param  string  $type       Type of ticket to set.
105
			 * @param  WP_User $user       Authenticated user associated with the ticket.
106
			 */
107
			$expiration = \apply_filters( 'cas_server_ticket_expiration', $expiration, $type, $user );
108
109
			$this->expires = microtime( true ) + $expiration;
110
111
			$this->markUnused();
112
		}
113
	}
114
115
	/**
116
	 * Magic method that returns the ticket as a string.
117
	 *
118
	 * @return string Ticket as string.
119
	 */
120
	public function __toString() {
121
		return $this->encodeTicket();
122
	}
123
124
	/**
125
	 * Create a new ticket instance from a ticket string.
126
	 *
127
	 * @param  string $string Ticket string.
128
	 * @return Ticket         Ticket object.
129
	 *
130
	 * @throws \Cassava\Exception\TicketException
131
	 *
132
	 * @uses \__()
133
	 * @uses \get_user_by()
134
	 * @uses \is_wp_error()
135
	 */
136
	public static function fromString( $string ) {
137
138
		$components = static::decodeTicket( $string );
139
140
		if ( $components['expires'] < time() ) {
141
			throw new TicketException( __( 'Expired ticket.', 'wp-cas-server' ) );
142
		}
143
144
		$user = \get_user_by( 'login', $components['login'] );
145
146
		if ( ! $user || \is_wp_error( $user ) ) {
147
			throw new TicketException( __( 'No user matches ticket.', 'wp-cas-server' ) );
148
		}
149
150
		$ticket = new static( $components['type'], $user, $components['service'], $components['expires'] );
151
152
		if ( $ticket->generateSignature() !== $components['signature'] ) {
153
			throw new TicketException( __( 'Corrupted ticket.', 'wp-cas-server' ) );
154
		}
155
156
		if ( $ticket->isUsed() ) {
157
			throw new TicketException( __( 'Unknown or used ticket.', 'wp-cas-server' ) );
158
		}
159
160
		return $ticket;
161
	}
162
163
	/**
164
	 * Serialize ticket components into a string.
165
	 *
166
	 * @return string Ticket string.
167
	 */
168
	protected function encodeTicket() {
169
		$components = array(
170
			$this->user->user_login,     // 1. User login
171
			urlencode( $this->service ), // 2. Service URL
172
			$this->expires,              // 3. Expiration timestamp
173
			$this->generateSignature(),  // 4. Cryptographic signature
174
		);
175
176
		return $this->type . '-' . base64_encode( implode( '|', $components ) );
177
	}
178
179
	/**
180
	 * Extracts components from a base64 encoded ticket string.
181
	 *
182
	 * @param  string $ticket Ticket string (minus the prefix).
183
	 * @return array          Ticket components.
184
	 *
185
	 * @throws \Cassava\Exception\TicketException
186
	 *
187
	 * @uses \__()
188
	 */
189
	protected static function decodeTicket( $ticket ) {
190
191
		if ( strpos( $ticket, '-' ) === false ) {
192
			$ticket = static::TYPE_ST . '-' . $ticket;
193
		}
194
195
		list( $type, $content ) = explode( '-', $ticket, 2 );
196
197
		$values = explode( '|', base64_decode( $content ) );
198
199
		if ( count( $values ) < 4 ) {
200
			throw new TicketException( __( 'Malformed ticket.', 'wp-cas-server' ) );
201
		}
202
203
		$keys       = array( 'login', 'service', 'expires', 'signature' );
204
		$components = array_combine( $keys, $values );
205
206
		$components['type']    = $type;
207
		$components['service'] = urldecode( $components['service'] );
208
209
		return $components;
210
	}
211
212
	/**
213
	 * Generate security key for a ticket.
214
	 *
215
	 * @return string Generated security key.
216
	 *
217
	 * @uses \wp_hash()
218
	 */
219
	protected function generateKey() {
220
		$keyComponents = array(
221
			$this->user->user_login,
222
			substr( $this->user->user_pass, 8, 4 ),
223
			$this->expires,
224
		);
225
		return \wp_hash( implode( '|', $keyComponents ) );
226
	}
227
228
	/**
229
	 * Create a ticket signature by concatenating components and signing them with a key.
230
	 *
231
	 * @return string      Generated signature hash.
232
	 */
233
	public function generateSignature() {
234
		$signatureComponents = array(
235
			$this->user->login,
236
			$this->service,
237
			$this->expires,
238
		);
239
		return hash_hmac( 'sha1', implode( '|', $signatureComponents ), $this->generateKey() );
240
	}
241
242
	/**
243
	 * Validates a ticket string against a list of expected types.
244
	 *
245
	 * @param  string $ticket Ticket string to validate.
246
	 * @param  array  $types  List of allowed type prefixes.
247
	 *
248
	 * @throws \Cassava\Exception\TicketException
249
	 */
250
	public static function validateAllowedTypes( $ticket, $types = array() ) {
251
		list( $type ) = explode( '-', $ticket, 2 );
252
253
		if ( ! in_array( $type, $types ) ) {
254
			throw new TicketException( __( 'Ticket type cannot be validated.', 'wp-cas-server' ) );
255
		}
256
	}
257
258
	/**
259
	 * Remember a fresh ticket using WordPress's Transients API.
260
	 *
261
	 * @uses \set_transient()
262
	 */
263
	protected function markUnused() {
264
		$key = $this->generateKey();
265
		\set_transient( Plugin::TRANSIENT_PREFIX . $key, (string) $this, $this->expires );
266
	}
267
268
	/**
269
	 * Remember a ticket as having been used using WordPress's Transients API.
270
	 *
271
	 * @uses \delete_transient()
272
	 *
273
	 * @todo "Remember-Me" tickets should not be invalidated.
274
	 */
275
	public function markUsed() {
276
		$key = $this->generateKey();
277
		\delete_transient( Plugin::TRANSIENT_PREFIX . $key );
278
	}
279
280
	/**
281
	 * Checks whether a ticket has been used using WordPress's Transients API.
282
	 *
283
	 * @return boolean Whether the ticket has been used.
284
	 *
285
	 * @uses \get_transient()
286
	 */
287
	public function isUsed() {
288
289
		if ( Options::get( 'allow_ticket_reuse' ) ) {
290
			return false;
291
		}
292
293
		$key = $this->generateKey();
294
295
		return ! \get_transient( Plugin::TRANSIENT_PREFIX . $key );
296
	}
297
298
}
299