github.com/hernad/nomad@v1.6.112/ui/app/controllers/settings/tokens.js (about) 1 /** 2 * Copyright (c) HashiCorp, Inc. 3 * SPDX-License-Identifier: MPL-2.0 4 */ 5 6 // @ts-check 7 import { inject as service } from '@ember/service'; 8 import Controller from '@ember/controller'; 9 import { getOwner } from '@ember/application'; 10 import { alias } from '@ember/object/computed'; 11 import { action } from '@ember/object'; 12 import classic from 'ember-classic-decorator'; 13 import { tracked } from '@glimmer/tracking'; 14 import Ember from 'ember'; 15 16 /** 17 * @type {RegExp} 18 */ 19 const JWT_MATCH_EXPRESSION = /^[a-zA-Z0-9-_]+\.[a-zA-Z0-9-_]+\.[a-zA-Z0-9-_]+$/; 20 21 @classic 22 export default class Tokens extends Controller { 23 @service token; 24 @service store; 25 @service router; 26 27 queryParams = ['code', 'state', 'jwtAuthMethod']; 28 29 @tracked secret = this.token.secret; 30 31 /** 32 * @type {(null | "success" | "failure" | "jwtFailure")} signInStatus 33 */ 34 @tracked 35 signInStatus = null; 36 37 @alias('token.selfToken') tokenRecord; 38 39 resetStore() { 40 this.store.unloadAll(); 41 } 42 43 @action 44 clearTokenProperties() { 45 this.token.setProperties({ 46 secret: undefined, 47 tokenNotFound: false, 48 }); 49 this.signInStatus = null; 50 // Clear out all data to ensure only data the anonymous token is privileged to see is shown 51 this.resetStore(); 52 this.token.reset(); 53 this.store.findAll('auth-method'); 54 } 55 56 /** 57 * @returns {import('@ember/array/mutable').default<import('../../models/auth-method').default>} 58 */ 59 get authMethods() { 60 return this.model?.authMethods || []; 61 } 62 63 get hasJWTAuthMethods() { 64 return this.authMethods.any((method) => method.type === 'JWT'); 65 } 66 67 get nonTokenAuthMethods() { 68 return this.authMethods.rejectBy('type', 'JWT'); 69 } 70 71 get JWTAuthMethods() { 72 return this.authMethods.filterBy('type', 'JWT'); 73 } 74 75 get JWTAuthMethodOptions() { 76 return this.JWTAuthMethods.map((method) => ({ 77 key: method.name, 78 label: method.name, 79 })); 80 } 81 82 get defaultJWTAuthMethod() { 83 return ( 84 this.JWTAuthMethods.findBy('default', true) || this.JWTAuthMethods[0] 85 ); 86 } 87 88 @action 89 setCurrentAuthMethod() { 90 if (!this.jwtAuthMethod) { 91 this.jwtAuthMethod = this.defaultJWTAuthMethod?.name; 92 } 93 } 94 95 /** 96 * @type {string} 97 */ 98 @tracked jwtAuthMethod; 99 100 /** 101 * @type {boolean} 102 */ 103 get currentSecretIsJWT() { 104 return this.secret?.length > 36 && this.secret.match(JWT_MATCH_EXPRESSION); 105 } 106 107 @action 108 async verifyToken() { 109 const { secret } = this; 110 /** 111 * @type {import('../../adapters/token').default} 112 */ 113 114 // Ember currently lacks types for getOwner: https://github.com/emberjs/ember.js/issues/19916 115 // @ts-ignore 116 const TokenAdapter = getOwner(this).lookup('adapter:token'); 117 118 const isJWT = secret.length > 36 && secret.match(JWT_MATCH_EXPRESSION); 119 120 if (isJWT) { 121 const methodName = this.jwtAuthMethod; 122 123 // If user passes a JWT token, but there is no JWT auth method, throw an error 124 if (!methodName) { 125 this.token.set('secret', undefined); 126 this.signInStatus = 'jwtFailure'; 127 return; 128 } 129 130 this.clearTokenProperties(); 131 132 // Set bearer token instead of findSelf etc. 133 TokenAdapter.loginJWT(secret, methodName).then( 134 (token) => { 135 this.token.setProperties({ 136 secret: token.secret, 137 tokenNotFound: false, 138 }); 139 this.set('secret', null); 140 141 // Clear out all data to ensure only data the new token is privileged to see is shown 142 this.resetStore(); 143 144 // Refetch the token and associated policies 145 this.token.get('fetchSelfTokenAndPolicies').perform().catch(); 146 147 this.signInStatus = 'success'; 148 }, 149 () => { 150 this.token.set('secret', undefined); 151 this.signInStatus = 'failure'; 152 } 153 ); 154 } else { 155 this.clearTokenProperties(); 156 this.token.set('secret', secret); 157 this.set('secret', null); 158 159 TokenAdapter.findSelf().then( 160 () => { 161 // Clear out all data to ensure only data the new token is privileged to see is shown 162 this.resetStore(); 163 164 // Refetch the token and associated policies 165 this.token.get('fetchSelfTokenAndPolicies').perform().catch(); 166 167 this.signInStatus = 'success'; 168 this.token.set('tokenNotFound', false); 169 }, 170 () => { 171 this.token.set('secret', undefined); 172 this.signInStatus = 'failure'; 173 } 174 ); 175 } 176 } 177 178 // Generate a 20-char nonce, using window.crypto to 179 // create a sufficiently-large output then trimming 180 generateNonce() { 181 let randomArray = new Uint32Array(10); 182 crypto.getRandomValues(randomArray); 183 return randomArray.join('').slice(0, 20); 184 } 185 186 @action redirectToSSO(method) { 187 const provider = method.name; 188 const nonce = this.generateNonce(); 189 190 window.localStorage.setItem('nomadOIDCNonce', nonce); 191 window.localStorage.setItem('nomadOIDCAuthMethod', provider); 192 193 let redirectURL; 194 if (Ember.testing) { 195 redirectURL = this.router.currentURL; 196 } else { 197 redirectURL = new URL(window.location.toString()); 198 redirectURL.search = ''; 199 redirectURL = redirectURL.href; 200 } 201 202 method 203 .getAuthURL({ 204 AuthMethodName: provider, 205 ClientNonce: nonce, 206 RedirectUri: redirectURL, 207 }) 208 .then(({ AuthURL }) => { 209 if (Ember.testing) { 210 this.router.transitionTo(AuthURL.split('/ui')[1]); 211 } else { 212 window.location = AuthURL; 213 } 214 }); 215 } 216 217 @tracked code = null; 218 @tracked state = null; 219 220 get isValidatingToken() { 221 if (this.code && this.state) { 222 this.validateSSO(); 223 return true; 224 } else { 225 return false; 226 } 227 } 228 229 async validateSSO() { 230 let redirectURL; 231 if (Ember.testing) { 232 redirectURL = this.router.currentURL; 233 } else { 234 redirectURL = new URL(window.location.toString()); 235 redirectURL.search = ''; 236 redirectURL = redirectURL.href; 237 } 238 239 const res = await this.token.authorizedRequest( 240 '/v1/acl/oidc/complete-auth', 241 { 242 method: 'POST', 243 body: JSON.stringify({ 244 AuthMethodName: window.localStorage.getItem('nomadOIDCAuthMethod'), 245 ClientNonce: window.localStorage.getItem('nomadOIDCNonce'), 246 Code: this.code, 247 State: this.state, 248 RedirectURI: redirectURL, 249 }), 250 } 251 ); 252 253 if (res.ok) { 254 const data = await res.json(); 255 this.clearTokenProperties(); 256 this.token.set('secret', data.SecretID); 257 this.state = null; 258 this.code = null; 259 260 // Refetch the token and associated policies 261 this.token.get('fetchSelfTokenAndPolicies').perform().catch(); 262 263 this.signInStatus = 'success'; 264 this.token.set('tokenNotFound', false); 265 } else { 266 this.state = 'failure'; 267 this.code = null; 268 } 269 } 270 271 get SSOFailure() { 272 return this.state === 'failure'; 273 } 274 275 get canSignIn() { 276 return !this.tokenRecord || this.tokenRecord.isExpired; 277 } 278 279 get shouldShowPolicies() { 280 return this.tokenRecord; 281 } 282 }