-
-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathPendingChange.php
More file actions
154 lines (140 loc) · 4.37 KB
/
Copy pathPendingChange.php
File metadata and controls
154 lines (140 loc) · 4.37 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
<?php
/**
* Pending email-change state stored as user_meta.
*
* @package WorkOS\Auth\ChangeEmail
*/
namespace WorkOS\Auth\ChangeEmail;
defined( 'ABSPATH' ) || exit;
/**
* Persists the pending email-change record for a single user.
*
* The full payload contains the *new* address, the hashed confirm token,
* the hashed cancel token (used by the "this wasn't me" link to the old
* address), the initiator, the issue time, and the absolute expiry. Only
* hashes — never plaintext tokens — sit in the database.
*
* Single-use is enforced by clearing the meta on confirm, cancel, or
* expiry. There is at most one pending change per user.
*/
class PendingChange {
public const META_KEY = '_workos_pending_email_change';
/**
* Token factory used for confirm-token verification.
*
* @var TokenFactory
*/
private TokenFactory $tokens;
/**
* Constructor.
*
* @param TokenFactory $tokens Token factory.
*/
public function __construct( TokenFactory $tokens ) {
$this->tokens = $tokens;
}
/**
* Persist a new pending change.
*
* @param int $user_id Target WP user ID.
* @param string $new_email Lowercased, sanitized new email.
* @param string $confirm_token Plaintext confirm token (hashed before storage).
* @param string $cancel_token Plaintext cancel token (hashed before storage).
* @param int $expires_at Unix timestamp at which the confirm token expires.
* @param int $initiated_by WP user ID of the initiator (0 for system).
*
* @return void
*/
public function store(
int $user_id,
string $new_email,
string $confirm_token,
string $cancel_token,
int $expires_at,
int $initiated_by
): void {
update_user_meta(
$user_id,
self::META_KEY,
[
'new_email' => $new_email,
'token_hash' => $this->tokens->hash( $confirm_token ),
'cancel_token_hash' => $this->tokens->hash( $cancel_token ),
'expires_at' => $expires_at,
'initiated_by' => $initiated_by,
'initiated_at' => time(),
]
);
}
/**
* Load the stored pending change for a user, or null if none exists.
*
* @param int $user_id WP user ID.
*
* @return array{new_email:string,token_hash:string,cancel_token_hash:string,expires_at:int,initiated_by:int,initiated_at:int}|null
*/
public function get( int $user_id ): ?array {
$stored = get_user_meta( $user_id, self::META_KEY, true );
if ( ! is_array( $stored ) ) {
return null;
}
// Defensive: only return well-formed records. Anything missing a
// required field is treated as "no pending change" rather than
// silently authenticating a half-filled meta row.
foreach ( [ 'new_email', 'token_hash', 'cancel_token_hash', 'expires_at' ] as $required ) {
if ( ! isset( $stored[ $required ] ) ) {
return null;
}
}
return [
'new_email' => (string) $stored['new_email'],
'token_hash' => (string) $stored['token_hash'],
'cancel_token_hash' => (string) $stored['cancel_token_hash'],
'expires_at' => (int) $stored['expires_at'],
'initiated_by' => (int) ( $stored['initiated_by'] ?? 0 ),
'initiated_at' => (int) ( $stored['initiated_at'] ?? 0 ),
];
}
/**
* Check whether a stored record is past its expiry.
*
* @param array $record Record as returned by {@see get()}.
*
* @return bool
*/
public function expired( array $record ): bool {
return (int) ( $record['expires_at'] ?? 0 ) <= time();
}
/**
* Verify a candidate confirm token against the stored record.
*
* @param array $record Record as returned by {@see get()}.
* @param string $candidate Plaintext token.
*
* @return bool
*/
public function verify_confirm( array $record, string $candidate ): bool {
return $this->tokens->verify( $candidate, (string) ( $record['token_hash'] ?? '' ) );
}
/**
* Verify a candidate cancel token against the stored record.
*
* @param array $record Record as returned by {@see get()}.
* @param string $candidate Plaintext token.
*
* @return bool
*/
public function verify_cancel( array $record, string $candidate ): bool {
return $this->tokens->verify( $candidate, (string) ( $record['cancel_token_hash'] ?? '' ) );
}
/**
* Clear the pending change for a user.
*
* @param int $user_id WP user ID.
*
* @return void
*/
public function clear( int $user_id ): void {
delete_user_meta( $user_id, self::META_KEY );
}
}