@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.)
hq.recaptime.dev/wiki/Phorge
phorge
phabricator
1<?php
2
3/**
4 * Authentication adapter for JIRA OAuth1.
5 */
6final class PhutilJIRAAuthAdapter extends PhutilOAuth1AuthAdapter {
7
8 // TODO: JIRA tokens expire (after 5 years) and we could surface and store
9 // that.
10
11 private $jiraBaseURI;
12 private $adapterDomain;
13 private $userInfo;
14
15 public function setJIRABaseURI($jira_base_uri) {
16 $this->jiraBaseURI = $jira_base_uri;
17 return $this;
18 }
19
20 public function getJIRABaseURI() {
21 return $this->jiraBaseURI;
22 }
23
24 protected function newAccountIdentifiers() {
25 // Make sure the handshake is finished; this method is used for its
26 // side effect by Auth providers.
27 $this->getHandshakeData();
28
29 $info = $this->getUserInfo();
30
31 // See T13493. Older versions of JIRA provide a "key" with a username or
32 // email address. Newer versions of JIRA provide a GUID "accountId".
33 // Intermediate versions of JIRA provide both.
34
35 $identifiers = array();
36
37 $account_key = idx($info, 'key');
38 if ($account_key !== null) {
39 $identifiers[] = $this->newAccountIdentifier($account_key);
40 }
41
42 $account_id = idx($info, 'accountId');
43 if ($account_id !== null) {
44 $identifiers[] = $this->newAccountIdentifier(
45 sprintf(
46 'accountId(%s)',
47 $account_id));
48 }
49
50 return $identifiers;
51 }
52
53 public function getAccountName() {
54 return idx($this->getUserInfo(), 'name');
55 }
56
57 public function getAccountImageURI() {
58 $avatars = idx($this->getUserInfo(), 'avatarUrls');
59 if ($avatars) {
60 return idx($avatars, '48x48');
61 }
62 return null;
63 }
64
65 public function getAccountRealName() {
66 return idx($this->getUserInfo(), 'displayName');
67 }
68
69 public function getAccountEmail() {
70 return idx($this->getUserInfo(), 'emailAddress');
71 }
72
73 public function getAdapterType() {
74 return 'jira';
75 }
76
77 public function getAdapterDomain() {
78 return $this->adapterDomain;
79 }
80
81 public function setAdapterDomain($domain) {
82 $this->adapterDomain = $domain;
83 return $this;
84 }
85
86 protected function getSignatureMethod() {
87 return 'RSA-SHA1';
88 }
89
90 protected function getRequestTokenURI() {
91 return $this->getJIRAURI('plugins/servlet/oauth/request-token');
92 }
93
94 protected function getAuthorizeTokenURI() {
95 return $this->getJIRAURI('plugins/servlet/oauth/authorize');
96 }
97
98 protected function getValidateTokenURI() {
99 return $this->getJIRAURI('plugins/servlet/oauth/access-token');
100 }
101
102 private function getJIRAURI($path) {
103 return rtrim($this->jiraBaseURI, '/').'/'.ltrim($path, '/');
104 }
105
106 private function getUserInfo() {
107 if ($this->userInfo === null) {
108 $this->userInfo = $this->newUserInfo();
109 }
110
111 return $this->userInfo;
112 }
113
114 private function newUserInfo() {
115 // See T13493. Try a relatively modern (circa early 2020) API call first.
116 try {
117 return $this->newJIRAFuture('rest/api/3/myself', 'GET')
118 ->resolveJSON();
119 } catch (Exception $ex) {
120 // If we failed the v3 call, assume the server version is too old
121 // to support this API and fall back to trying the older method.
122 }
123
124 $session = $this->newJIRAFuture('rest/auth/1/session', 'GET')
125 ->resolveJSON();
126
127 // The session call gives us the username, but not the user key or other
128 // information. Make a second call to get additional information.
129
130 $params = array(
131 'username' => $session['name'],
132 );
133
134 return $this->newJIRAFuture('rest/api/2/user', 'GET', $params)
135 ->resolveJSON();
136 }
137
138 public static function newJIRAKeypair() {
139 $config = array(
140 'digest_alg' => 'sha512',
141 'private_key_bits' => 4096,
142 'private_key_type' => OPENSSL_KEYTYPE_RSA,
143 );
144
145 $res = openssl_pkey_new($config);
146 if (!$res) {
147 throw new Exception(pht('%s failed!', 'openssl_pkey_new()'));
148 }
149
150 $private_key = null;
151 $ok = openssl_pkey_export($res, $private_key);
152 if (!$ok) {
153 throw new Exception(pht('%s failed!', 'openssl_pkey_export()'));
154 }
155
156 $public_key = openssl_pkey_get_details($res);
157 if (!$ok || empty($public_key['key'])) {
158 throw new Exception(pht('%s failed!', 'openssl_pkey_get_details()'));
159 }
160 $public_key = $public_key['key'];
161
162 return array($public_key, $private_key);
163 }
164
165
166 /**
167 * JIRA indicates that the user has clicked the "Deny" button by passing a
168 * well known `oauth_verifier` value ("denied"), which we check for here.
169 */
170 protected function willFinishOAuthHandshake() {
171 $jira_magic_word = 'denied';
172 if ($this->getVerifier() == $jira_magic_word) {
173 throw new PhutilAuthUserAbortedException();
174 }
175 }
176
177 public function newJIRAFuture($path, $method, $params = array()) {
178 if ($method == 'GET') {
179 $uri_params = $params;
180 $body_params = array();
181 } else {
182 // For other types of requests, JIRA expects the request body to be
183 // JSON encoded.
184 $uri_params = array();
185 $body_params = phutil_json_encode($params);
186 }
187
188 $uri = new PhutilURI($this->getJIRAURI($path), $uri_params);
189
190 // JIRA returns a 415 error if we don't provide a Content-Type header.
191
192 return $this->newOAuth1Future($uri, $body_params)
193 ->setMethod($method)
194 ->addHeader('Content-Type', 'application/json');
195 }
196
197}