go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/milo/ui/src/common/api/auth_state/auth_state.ts (about)

     1  // Copyright 2023 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  import { get as kvGet, set as kvSet } from 'idb-keyval';
    16  
    17  export const ANONYMOUS_IDENTITY = 'anonymous:anonymous';
    18  
    19  export interface AuthState {
    20    readonly identity: string;
    21    readonly email?: string;
    22    readonly picture?: string;
    23    readonly accessToken?: string;
    24    readonly idToken?: string;
    25    /**
    26     * Expiration time (unix timestamp) of the access token.
    27     *
    28     * If zero/undefined, the access token does not expire.
    29     */
    30    readonly accessTokenExpiry?: number;
    31    /**
    32     * Expiration time (unix timestamp) of the ID token.
    33     *
    34     * If zero/undefined, the ID token does not expire.
    35     */
    36    readonly idTokenExpiry?: number;
    37  }
    38  
    39  const AUTH_STATE_KEY = 'auth-state-v2';
    40  
    41  // In-memory cache. Can be used to access the cache synchronously.
    42  let cachedAuthState: AuthState | null = null;
    43  
    44  /**
    45   * Update the auth state in IndexDB and the in-memory cache.
    46   */
    47  export function setAuthStateCache(authState: AuthState | null): Promise<void> {
    48    cachedAuthState = authState;
    49    return kvSet(AUTH_STATE_KEY, authState);
    50  }
    51  
    52  /**
    53   * Gets auth state synchronously. Returns null if
    54   * 1. the auth state is not cached in memory, or
    55   * 2. the auth state has expired.
    56   */
    57  export function getAuthStateCacheSync() {
    58    return cachedAuthState && msToExpire(cachedAuthState) > 0
    59      ? cachedAuthState
    60      : null;
    61  }
    62  
    63  /**
    64   * Gets auth state and populate the in-memory cache. Returns null if
    65   * 1. the auth state is not cached in IndexDB, or
    66   * 2. the auth state has expired.
    67   */
    68  export async function getAuthStateCache() {
    69    cachedAuthState = (await kvGet<AuthState | null>(AUTH_STATE_KEY)) || null;
    70    return getAuthStateCacheSync();
    71  }
    72  
    73  /**
    74   * Gets the auth state associated with the current section from the server.
    75   *
    76   * Also populates the cached auth state up on successfully retrieving the auth
    77   * state.
    78   */
    79  export async function queryAuthState(fetchImpl = fetch): Promise<AuthState> {
    80    // `self.origin` isn't prepended to the path automatically in unit tests,
    81    // which can lead to surprising behaviors in some cases (e.g. string
    82    // matching). Do it manually here to make it easier to write unit tests.
    83    const res = await fetchImpl(self.origin + '/auth/openid/state');
    84    if (!res.ok) {
    85      throw new Error('failed to get auth state:\n' + (await res.text()));
    86    }
    87  
    88    return res.json();
    89  }
    90  
    91  /**
    92   * Obtains a current auth state.
    93   * Use the cached auth state if it's not expired yet.
    94   * Refresh the cached auth state otherwise.
    95   */
    96  export async function obtainAuthState() {
    97    let authState = await getAuthStateCache();
    98    if (authState) {
    99      return authState;
   100    }
   101  
   102    authState = await queryAuthState();
   103    setAuthStateCache(authState);
   104  
   105    return authState;
   106  }
   107  
   108  /**
   109   * Returns the time to expire in milliseconds.
   110   */
   111  export function msToExpire(authState: AuthState) {
   112    const expiry = Math.min(
   113      authState.idTokenExpiry || Infinity,
   114      authState.accessTokenExpiry || Infinity,
   115    );
   116    return expiry * 1000 - Date.now();
   117  }