All files / utils auth.ts

87.5% Statements 42/48
100% Branches 21/21
72.72% Functions 8/11
87.5% Lines 42/48

Press n or j to go to the next uncovered block, b, p or k for the previous block.

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              11x 11x 11x 2x           13x 1x     12x 12x 2x         10x 10x 1x   9x 1x     8x       8x 8x 8x       8x     8x 8x   1x         7x 7x       170x                       13x 13x   13x 13x 13x         1x   5x   2x 1x   1x                     13x   13x 12x 12x 3x   9x     13x                                              
import { ref } from "vue";
 
import { useDBSLoginWebcomponentPlugin } from "@/components/DBSLoginWebcomponentPlugin";
import AuthorizationEventDetails from "@/types/AuthorizationEventDetails";
 
class JwtParseError extends Error {
  constructor(message: string, options?: { cause?: unknown }) {
    super(message);
    this.name = "JwtParseError";
    if (options?.cause !== undefined) {
      this.cause = options.cause;
    }
  }
}
 
function getJwtPayloadSegment(token: string): string {
  if (!token?.trim()) {
    throw new JwtParseError("Invalid JWT: token must be a non-empty string");
  }
 
  const parts = token.split(".");
  if (parts.length !== 3) {
    throw new JwtParseError(
      `Invalid JWT: expected 3 dot-separated segments, got ${parts.length}`
    );
  }
 
  const [headerSegment, payloadSegment] = parts;
  if (!headerSegment) {
    throw new JwtParseError("Invalid JWT: header segment is missing");
  }
  if (!payloadSegment) {
    throw new JwtParseError("Invalid JWT: payload segment is missing");
  }
 
  return payloadSegment;
}
 
function base64UrlToBase64(base64Url: string): string {
  const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
  const paddingLength = (4 - (base64.length % 4)) % 4;
  return base64 + "=".repeat(paddingLength);
}
 
function decodeJwtPayloadSegment(payloadSegment: string): string {
  const base64 = base64UrlToBase64(payloadSegment);
 
  let decoded: string;
  try {
    decoded = window.atob(base64);
  } catch (error) {
    throw new JwtParseError("Invalid JWT: failed to base64-decode payload", {
      cause: error,
    });
  }
 
  try {
    return decodeURIComponent(
      decoded
        .split("")
        .map((character) => {
          return "%" + ("00" + character.charCodeAt(0).toString(16)).slice(-2);
        })
        .join("")
    );
  } catch (error) {
    throw new JwtParseError("Invalid JWT: failed to decode payload bytes", {
      cause: error,
    });
  }
}
 
function parseJwt(token: string): Record<string, unknown> {
  const payloadSegment = getJwtPayloadSegment(token);
  const jsonPayload = decodeJwtPayloadSegment(payloadSegment);
 
  try {
    const parsed: unknown = JSON.parse(jsonPayload);
    if (
      parsed === null ||
      typeof parsed !== "object" ||
      Array.isArray(parsed)
    ) {
      throw new JwtParseError("Invalid JWT: payload must be a JSON object");
    }
    return parsed as Record<string, unknown>;
  } catch (error) {
    if (error instanceof JwtParseError) {
      throw error;
    }
    throw new JwtParseError("Invalid JWT: payload is not valid JSON", {
      cause: error,
    });
  }
}
 
export function getTokenData(accessToken: string): {
  email?: string;
  given_name?: string;
  family_name?: string;
} {
  const payload = parseJwt(accessToken);
 
  const getOptionalString = (key: string): string | undefined => {
    const value = payload[key];
    if (value !== undefined && typeof value !== "string") {
      throw new JwtParseError(`Invalid JWT: ${key} must be a string`);
    }
    return value as string | undefined;
  };
 
  return {
    email: getOptionalString("email"),
    given_name: getOptionalString("given_name"),
    family_name: getOptionalString("family_name"),
  };
}
 
export function useLogin() {
  const accessToken = ref<string | null>(null);
  const { loggedIn, loading } = useDBSLoginWebcomponentPlugin(
    (authEventDetails: AuthorizationEventDetails) => {
      accessToken.value = authEventDetails.accessToken;
    },
    () => {
      accessToken.value = null;
    }
  );
  return {
    isLoggedIn: loggedIn,
    isLoadingAuthentication: loading,
    accessToken,
  };
}