├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── authui-container ├── CHANGELOG.md ├── Dockerfile ├── README.md ├── cloudbuild.json ├── common │ ├── config-builder.ts │ ├── config.ts │ ├── deep-copy.ts │ ├── index.ts │ └── validator.ts ├── deploy-container.sh ├── karma.config.js ├── package-lock.json ├── package.json ├── public │ └── style.css ├── server │ ├── api │ │ ├── authenticated-request-handler.ts │ │ ├── cloud-storage-handler.ts │ │ ├── gcip-handler.ts │ │ ├── iap-settings-handler.ts │ │ ├── metadata-server.ts │ │ └── token-manager.ts │ ├── app.ts │ ├── auth-server.ts │ ├── templates.ts │ └── utils │ │ ├── error.ts │ │ └── http-server-request-handler.ts ├── src │ ├── admin-ui.ts │ ├── admin.ts │ ├── polyfill.ts │ ├── script.ts │ ├── sign-in-ui.ts │ └── utils │ │ ├── FirebaseWrapper.ts │ │ ├── browser.ts │ │ ├── http-client.ts │ │ ├── index.ts │ │ └── validator.ts ├── test │ └── unit │ │ ├── common │ │ ├── config-builder.spec.ts │ │ ├── deep-copy.spec.ts │ │ ├── index.spec.ts │ │ └── validator.spec.ts │ │ ├── server │ │ ├── api │ │ │ ├── authenticated-request-handler.spec.ts │ │ │ ├── cloud-storage-handler.spec.ts │ │ │ ├── gcip-handler.spec.ts │ │ │ ├── iap-settings-handler.spec.ts │ │ │ ├── metadata-server.spec.ts │ │ │ └── token-manager.spec.ts │ │ ├── app.spec.ts │ │ ├── auth-server.spec.ts │ │ └── utils │ │ │ └── http-server-request-handler.spec.ts │ │ └── src │ │ ├── admin-ui.spec.ts │ │ ├── sign-in-ui.spec.ts │ │ ├── test-utils.ts │ │ └── utils │ │ ├── browser.spec.ts │ │ ├── http-client.spec.ts │ │ ├── index.spec.ts │ │ └── validator.spec.ts ├── tsconfig.json ├── tsconfig.server.json ├── tsconfig.webpack.json ├── tslint-test.json ├── tslint.json └── webpack.config.js ├── package-lock.json ├── package.json ├── package └── gcip-cloud-functions │ └── README.md └── sample ├── app ├── README.md ├── app.yaml ├── package-lock.json ├── package.json ├── server │ ├── app.js │ ├── templates.js │ └── verify-iap-jwt.js └── styles │ └── styles.css ├── authui-firebaseui ├── README.md ├── database.rules.json ├── firebase.json ├── package-lock.json ├── package.json ├── public │ ├── 404.html │ ├── index.html │ └── style.css ├── src │ └── script.ts ├── tsconfig.json └── webpack.config.js ├── authui-react ├── .env ├── README.md ├── database.rules.json ├── firebase.json ├── package-lock.json ├── package.json ├── public │ ├── index.html │ ├── manifest.json │ └── robots.txt ├── src │ ├── components │ │ ├── alert.tsx │ │ ├── app.tsx │ │ ├── firebaseui.tsx │ │ ├── navbar.tsx │ │ ├── privacypolicy.tsx │ │ ├── progressbar.tsx │ │ ├── root.tsx │ │ ├── selecttenant.tsx │ │ ├── signin.tsx │ │ ├── signinwithemail.tsx │ │ ├── signout.tsx │ │ └── signupwithemail.tsx │ ├── index.css │ ├── index.tsx │ ├── react-app-env.d.ts │ └── serviceWorker.ts └── tsconfig.json └── authui ├── README.md ├── angular.json ├── database.rules.json ├── firebase.json ├── package-lock.json ├── package.json ├── src ├── app │ ├── alert.component.ts │ ├── app.component.ts │ ├── app.module.ts │ ├── firebaseui.component.ts │ ├── navbar.component.ts │ ├── pagenotfound.component.ts │ ├── privacypolicy.component.ts │ ├── progressbar.component.ts │ ├── root.component.ts │ ├── selecttenant.component.ts │ ├── signin.component.ts │ ├── signinwithemail.component.ts │ ├── signout.component.ts │ └── signupwithemail.component.ts ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── index.html ├── karma.conf.js ├── main.ts ├── polyfills.ts ├── styles.css └── test.ts ├── tsconfig.app.json ├── tsconfig.base.json ├── tsconfig.json └── tsconfig.spec.json /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .vscode 3 | .DS_Store 4 | npm-debug.log 5 | default.profraw 6 | 7 | lib/ 8 | dist/ 9 | .tmp/ 10 | .rpt2_cache/ 11 | typings/ 12 | coverage/ 13 | node_modules/ 14 | builds/ciap/ 15 | tmp/ 16 | .nyc_output/ 17 | .firebase/ 18 | 19 | # Generated files. 20 | *.log 21 | *.pyc 22 | .DS_Store 23 | *~ 24 | *.swp 25 | *.svg 26 | 27 | # Do not check in any configuration related data. 28 | **/.firebaserc 29 | 30 | # Exclude generated files. 31 | sample/authui-react/build/ 32 | sample/authui/dist/ 33 | sample/authui-firebaseui/public/script.js 34 | 35 | # Real key file and other project information should not be checked in 36 | test/resources/key*.json 37 | 38 | # Release tarballs should not be checked in 39 | gcip-iap-*.tgz 40 | firebase.tgz 41 | 42 | # Do not check in generated JS files in authui-container. 43 | authui-container/public/admin.js 44 | authui-container/public/script.js 45 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to accept your patches and contributions to this project. There are 4 | just a few small guidelines you need to follow. 5 | 6 | ## Contributor License Agreement 7 | 8 | Contributions to this project must be accompanied by a Contributor License 9 | Agreement. You (or your employer) retain the copyright to your contribution; 10 | this simply gives us permission to use and redistribute your contributions as 11 | part of the project. Head over to to see 12 | your current agreements on file or to sign a new one. 13 | 14 | You generally only need to submit a CLA once, so if you've already submitted one 15 | (even if it was for a different project), you probably don't need to do it 16 | again. 17 | 18 | ## Code reviews 19 | 20 | All submissions, including submissions by project members, require review. We 21 | use GitHub pull requests for this purpose. Consult 22 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 23 | information on using pull requests. 24 | 25 | ## Community Guidelines 26 | 27 | This project follows [Google's Open Source Community 28 | Guidelines](https://opensource.google.com/conduct/). -------------------------------------------------------------------------------- /authui-container/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | #v0.1.0 3 | 4 | gcr.io/gcip-iap/authui@sha256:11a871958769d45544794db8c22af958279817a213717e4f793993cc6433bc99 5 | 6 | Launch first version of the Cloud Run Hosted UI for IAP external identities. 7 | 8 | #v0.1.1 9 | 10 | gcr.io/gcip-iap/authui@sha256:7805cdc257ce5296aaa7ebe7addc16628fc6596a013d82c679b93983d7592a7d 11 | 12 | Ensures that GCS bucket names for custom configs meet [bucket name requirements](https://cloud.google.com/storage/docs/naming-buckets#requirements). 13 | This includes trimming characters beyond the 63 character limit and requiring the last character to be a letter or number. 14 | 15 | #v0.1.2 16 | 17 | gcr.io/gcip-iap/authui@sha256:a7dc58008bb7fb2510cb78c8c834f3b9d6ab9d03919b8c261bcb22e4cc7a848d 18 | 19 | Updates `firebaseui` version. The latest update supports customization of IdP buttons like Google, Email, etc. This includes the ability to overwrite the entire button label via `fullLabel` field. 20 | 21 | #v0.1.3 22 | 23 | gcr.io/gcip-iap/authui@sha256:3e2c606a2cce4944c9f7e816dacc968e3306d0a3aea6f14892b9837241497273 24 | 25 | Updates `gcip-iap` module dependency to handle browsers with 3P cookies and site data disabled. 26 | 27 | #v0.1.4 28 | 29 | gcr.io/gcip-iap/authui@sha256:5c3030f22afffba367c3106acb3674bbbee7be826c9b9de0c64e1b26885e64f6 30 | 31 | Updates `firebaseui` version. The latest update supports customization of tenant selection buttons. This includes the ability to overwrite the entire button label via `fullLabel` field. 32 | 33 | #v0.1.5 34 | 35 | gcr.io/gcip-iap/authui@sha256:d74bcbc4ba8797da73bc98f1a2695625eb7df78eb5bb4405213265f8a2350baa 36 | 37 | Fixes authui-container support in pre-Chromium Edge and IE 11 browsers. 38 | 39 | #v0.1.6 40 | 41 | gcr.io/gcip-iap/authui@sha256:b7d58fed82542e39f12ac7d920c61629bde7f8baa3ba7abc5b2def7c16f6cb57 42 | 43 | Fixes unexpected fragment parsing when determining selected tenant info. 44 | 45 | #v0.1.7 46 | 47 | gcr.io/gcip-iap/authui@sha256:362f671689af6b265c8dbd59caf99d06fae1af425758df121f39e8193766d270 48 | 49 | Updates `firebaseui`, `firebase` versions and ES6 firebase/app import. 50 | 51 | #v0.1.8 52 | 53 | gcr.io/gcip-iap/authui@sha256:fa6d5e92351a4683235b5aaf1e126509667954a2ad5afa01b5f73f4e09f41ce0 54 | 55 | Updates `gcip-iap` module dependency to `0.1.4`. 56 | 57 | #v0.1.9 58 | 59 | gcr.io/gcip-iap/authui@sha256:db842770d654782001b7aba24532458fdb9ef9953b70ea2fcd162b2a133abf29 60 | 61 | Updates `firebaseui` dependency version to `4.8.0` to support disabling new user sign up in email providers, disabling new user sign up via project settings and blocking functions. 62 | 63 | #v0.1.10 64 | 65 | gcr.io/gcip-iap/authui@sha256:c9bc65e24793d5f69bfd5bfa3b518defd80ee39ee1a0efedac99b2b2c6ffba07 66 | 67 | Fixes GCS file/bucket not found error handling. 68 | 69 | #v0.1.11 70 | 71 | gcr.io/gcip-iap/authui@sha256:7703925296db259590bae47e7004118d75f1727a132b742e76991a8407755905 72 | 73 | Updates `firebaseui` dependency version to `4.8.1` to provide UI support for all identity provider sign up operations when they are restricted by site administrators. 74 | 75 | #v0.1.12 76 | 77 | gcr.io/gcip-iap/authui@sha256:b87f3da6be2981a4182e6877855aec7c5d6c20c95c457877f2903cc47b27084f 78 | 79 | Updates `gcip-iap` module dependency to `1.0.0`, `firebase` module to `9.6.0` and `firebaseui` to `6.0.0`. 80 | 81 | #v0.1.13 82 | 83 | gcr.io/gcip-iap/authui@sha256:ce274e63d3eb1e9ba526e6530a6ba6c8550ed746cfba2236941d648a31464e44 84 | 85 | Use the login page URL as authDomain for Hosted UI to prevent cross origin storage access in signInWithRedirect. 86 | 87 | ##v1.0.0 88 | 89 | gcr.io/gcip-iap/authui@sha256:9d60aa020b50949a8c91fb102813dc5a37887680baebe878a5236996a8691573 90 | 91 | Use modular web SDK v9 to refactor IAP SDK. 92 | 93 | 94 | #v1.0.1 95 | 96 | gcr.io/gcip-iap/authui@sha256:27c7908ccc66941a89e4774a858d75d514a0422ab4d9b0600bd41332d9e57bd1 97 | 98 | Update the admin UI to use the IAP page URL as the authDomain. 99 | 100 | 101 | #v1.0.2 102 | 103 | gcr.io/gcip-iap/authui@sha256:eca6362af482a523c3d7f889728c8c36e2ebbc394a1a5033666456b2fd2fa8ff 104 | 105 | Updated authui-container to use latest IAP SDK. 106 | 107 | #v1.0.3 108 | 109 | gcr.io/gcip-iap/authui@sha256:ccd62095da439d2685df163eb2ce61ce7d709f7fb88bd98cedf3bcbe5fcfe218 110 | 111 | Dependency version bump to fix known vulnerabilities in the older versions 112 | 113 | #v1.0.4 114 | 115 | gcr.io/gcip-iap/authui@sha256:1fdafdadd1c723cdcb7287984a90f7b5a156b0ffa292f162891df8cfd03682c0 116 | 117 | Fix /versionz URL which is supposed to display the version of authui-container. 118 | 119 | #v1.0.5 120 | 121 | gcr.io/gcip-iap/authui@sha256:160419e85324f51889bc9e6f972cdbaf61bec3c2d2accb883da9d579c5b4d7dd 122 | 123 | Bump dependencies(http-proxy-middleware, tough-cookie, cookie, css-loader, @types/supertest) to fix vulnerabilities 124 | -------------------------------------------------------------------------------- /authui-container/Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google Inc. All Rights Reserved. 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 | # Use the official lightweight Node.js 22 image. 16 | # https://hub.docker.com/_/node 17 | FROM node:22-slim 18 | 19 | # Update OS image 20 | RUN apt update && apt -y upgrade 21 | RUN apt -y autoremove 22 | 23 | # Create and change to the app directory. 24 | WORKDIR /usr/src/app 25 | 26 | # Copy application dependency manifests to the container image. 27 | # A wildcard is used to ensure both package.json AND package-lock.json are copied. 28 | # Copying this separately prevents re-running npm install on every code change. 29 | COPY package*.json ./ 30 | 31 | # Install production dependencies. 32 | RUN npm install --only=production 33 | 34 | # Copy local code to the container image. 35 | COPY . ./ 36 | 37 | # Run the web service on container startup. 38 | CMD [ "sh","-c","npm start" ] -------------------------------------------------------------------------------- /authui-container/cloudbuild.json: -------------------------------------------------------------------------------- 1 | { 2 | "steps": [ 3 | { 4 | "name": "gcr.io/cloud-builders/docker", 5 | "args": [ 6 | "build", 7 | "-t", 8 | "", 9 | "." 10 | ] 11 | } 12 | ], 13 | "images": [ 14 | "" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /authui-container/common/config.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the 10 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | * express or implied. See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | export interface GcipConfig { 16 | apiKey: string; 17 | authDomain: string; 18 | } 19 | 20 | export interface TenantUiConfigSignInOption { 21 | provider: string; 22 | providerName?: string; 23 | } 24 | 25 | interface DisableSignUpConfig { 26 | status: boolean; 27 | adminEmail?: string; 28 | helpLink?: string; 29 | } 30 | 31 | export interface TenantUiConfig { 32 | fullLabel?: string; 33 | displayName?: string; 34 | signInOptions: TenantUiConfigSignInOption[]; 35 | adminRestrictedOperation?: DisableSignUpConfig; 36 | } 37 | 38 | export interface SignInOption { 39 | provider: string; 40 | fullLabel?: string; 41 | providerName?: string; 42 | hd?: string; 43 | buttonColor?: string; 44 | iconUrl?: string; 45 | scopes?: string[]; 46 | customParameters?: {[key: string]: any}; 47 | loginHintKey?: string; 48 | requireDisplayName?: boolean; 49 | recaptchaParameters?: { 50 | type?: string; 51 | size?: string; 52 | badge?: string; 53 | }; 54 | defaultCountry?: string; 55 | defaultNationalNumber?: string; 56 | loginHint?: string; 57 | whitelistedCountries?: string[]; 58 | blacklistedCountries?: string[]; 59 | disableSignUp?: DisableSignUpConfig; 60 | } 61 | 62 | export interface ExtendedTenantUiConfig { 63 | fullLabel?: string; 64 | displayName: string; 65 | iconUrl: string; 66 | logoUrl?: string; 67 | buttonColor: string; 68 | signInOptions: (SignInOption | string)[]; 69 | tosUrl?: string; 70 | privacyPolicyUrl?: string; 71 | immediateFederatedRedirect?: boolean; 72 | signInFlow?: 'redirect' | 'popup'; 73 | adminRestrictedOperation?: DisableSignUpConfig; 74 | } 75 | 76 | export interface UiConfig { 77 | [key: string]: { 78 | authDomain?: string; 79 | displayMode: string; 80 | selectTenantUiTitle?: string; 81 | selectTenantUiLogo?: string; 82 | styleUrl?: string; 83 | tenants: { 84 | [key: string]: ExtendedTenantUiConfig; 85 | }; 86 | tosUrl?: string, 87 | privacyPolicyUrl?: string, 88 | }; 89 | } 90 | -------------------------------------------------------------------------------- /authui-container/common/deep-copy.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright 2020 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * Returns a deep copy of an object or array. 19 | * 20 | * @param value The object or array to deep copy. 21 | * @return A deep copy of the provided object or array. 22 | */ 23 | export function deepCopy(value: T): T { 24 | return deepExtend(undefined, value); 25 | } 26 | 27 | /** 28 | * Checks for deep equality between the provided parameters. 29 | * https://stackoverflow.com/questions/25456013/javascript-deepequal-comparison/25456134 30 | * 31 | * @param a The first object to be compared for deep equality. 32 | * @param b The second object to be compared for deep equality. 33 | * @return Whether a deep equals b. 34 | */ 35 | export function deepEqual(a: any, b: any): boolean { 36 | if (a === b) { 37 | return true; 38 | } else if (typeof a === 'object' && 39 | typeof b === 'object' && 40 | a !== null && 41 | b !== null && 42 | Object.keys(a).length === Object.keys(b).length) { 43 | // Match properties one by one. 44 | for (const prop in a) { 45 | if (a.hasOwnProperty(prop)) { 46 | if (!b.hasOwnProperty(prop) || 47 | !deepEqual(a[prop], b[prop])) { 48 | return false; 49 | } 50 | } 51 | } 52 | // All sub properties match. 53 | return true; 54 | } 55 | return false; 56 | } 57 | 58 | 59 | /** 60 | * Copies properties from source to target (recursively allows extension of objects and arrays). 61 | * Scalar values in the target are over-written. If target is undefined, an object of the 62 | * appropriate type will be created (and returned). 63 | * 64 | * We recursively copy all child properties of plain objects in the source - so that namespace-like 65 | * objects are merged. 66 | * 67 | * Note that the target can be a function, in which case the properties in the source object are 68 | * copied onto it as static properties of the function. 69 | * 70 | * @param target The value which is being extended. 71 | * @param source The value whose properties are extending the target. 72 | * @return The target value. 73 | */ 74 | export function deepExtend(target: any, source: any): any { 75 | if (!(source instanceof Object)) { 76 | return source; 77 | } 78 | switch (source.constructor) { 79 | case Date: 80 | // Treat Dates like scalars; if the target date object had any child 81 | // properties - they will be lost! 82 | const dateValue = (source as any) as Date; 83 | return new Date(dateValue.getTime()); 84 | 85 | case Object: 86 | if (target === undefined) { 87 | target = {}; 88 | } 89 | break; 90 | 91 | case Array: 92 | // Always copy the array source and overwrite the target. 93 | target = []; 94 | break; 95 | 96 | default: 97 | // Not a plain Object - treat it as a scalar. 98 | return source; 99 | } 100 | 101 | for (const prop in source) { 102 | if (!source.hasOwnProperty(prop)) { 103 | continue; 104 | } 105 | target[prop] = deepExtend(target[prop], source[prop]); 106 | } 107 | 108 | return target; 109 | } 110 | -------------------------------------------------------------------------------- /authui-container/common/index.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright 2020 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import {isNonNullObject} from './validator'; 18 | 19 | // REGEX pattern for safe URL. 20 | const SAFE_URL_PATTERN = /^(?:(?:https?|mailto|ftp):|[^:/?#]*(?:[/?#]|$))/i; 21 | 22 | /** 23 | * The innocuous string returned when an unsafe URL is to be sanitized. 24 | * about:invalid is registered in 25 | * http://www.w3.org/TR/css3-values/#about-invalid. 26 | */ 27 | const INNOCUOUS_STRING = 'about:invalid'; 28 | 29 | /** 30 | * Defines a new read-only property directly on an object and returns the object. 31 | * 32 | * @param obj The object on which to define the property. 33 | * @param prop The name of the property to be defined or modified. 34 | * @param value The value associated with the property. 35 | */ 36 | export function addReadonlyGetter(obj: object, prop: string, value: any): void { 37 | Object.defineProperty(obj, prop, { 38 | value, 39 | // Make this property read-only. 40 | writable: false, 41 | // Include this property during enumeration of obj's properties. 42 | enumerable: true, 43 | }); 44 | } 45 | 46 | /** 47 | * Removes entries in an object whose values are undefined and returns the same 48 | * object. This only removes the top-level undefined fields. 49 | * 50 | * @param obj The object whose undefined fields are to be removed. 51 | * @return The same object with undefined fields removed. 52 | */ 53 | export function removeUndefinedFields(obj: T): T { 54 | // If obj is not a non-null object, return it back. 55 | if (!isNonNullObject(obj)) { 56 | return obj; 57 | } 58 | for (const key in obj) { 59 | if (typeof obj[key] === 'undefined') { 60 | delete obj[key]; 61 | } 62 | } 63 | return obj; 64 | } 65 | 66 | /** 67 | * Formats a string of form 'project/{projectId}/{api}' and replaces 68 | * with corresponding arguments {projectId: '1234', api: 'resource'} 69 | * and returns output: 'project/1234/resource'. 70 | * 71 | * @param str The original string where the param need to be 72 | * replaced. 73 | * @param params The optional parameters to replace in the 74 | * string. 75 | * @return The resulting formatted string. 76 | */ 77 | export function formatString(str: string, params?: {[key: string]: string}): string { 78 | let formatted = str; 79 | Object.keys(params || {}).forEach((key) => { 80 | formatted = formatted.replace( 81 | new RegExp('{' + key + '}', 'g'), 82 | (params as {[key: string]: string})[key]); 83 | }); 84 | return formatted; 85 | } 86 | 87 | /** 88 | * Maps an object's value based on the provided callback function. 89 | * 90 | * @param obj The object to map. 91 | * @param cb The callback function used to compute the new mapped value. 92 | * @return The mapped new object. 93 | */ 94 | export function mapObject( 95 | obj: {[key: string]: T}, 96 | cb: (key: string, value: T) => V): {[key: string]: V} { 97 | const mappedObject: {[key: string]: V} = {}; 98 | Object.keys(obj).forEach((key: string) => { 99 | mappedObject[key] = cb(key, obj[key]); 100 | }); 101 | return mappedObject; 102 | } 103 | 104 | /** 105 | * Sanitizes the URL provided. 106 | * 107 | * @param url The unsanitized URL. 108 | * @return The sanitized URL. 109 | */ 110 | export function sanitizeUrl(url: string): string { 111 | if (!isSafeUrl(url)) { 112 | return INNOCUOUS_STRING; 113 | } 114 | return url; 115 | } 116 | 117 | /** 118 | * @param url The URL to validate for safety. 119 | * @return Whether the URL is safe to use. 120 | */ 121 | export function isSafeUrl(url: string): boolean { 122 | return SAFE_URL_PATTERN.test(url); 123 | } 124 | 125 | /** 126 | * @param str The string to check. 127 | * @return Whether the string ends with a letter or number. 128 | */ 129 | export function isLastCharLetterOrNumber(str: string): boolean { 130 | const lastChar = str.charAt(str.length - 1); 131 | return !!lastChar.match(/[a-z0-9]/i); 132 | } 133 | -------------------------------------------------------------------------------- /authui-container/deploy-container.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright 2020 Google Inc. All Rights Reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS-IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | # This script is used to deploy a new version of the containerized Auth UI. 17 | # Sign in to gcloud with editor role in project ID gcip-iap is required. 18 | 19 | # GCP project ID. 20 | PROJECT_ID="gcip-iap" 21 | # GCR container image name. 22 | IMG_NAME="authui" 23 | # Version placeholder to replace in file. 24 | VERSION_PLACEHOLDER="__XXX_HOSTED_UI_VERSION_XXX__" 25 | # GCR path where the image is stored 26 | GCR_PATH=gcr.io/$PROJECT_ID/$IMG_NAME 27 | 28 | printusage() { 29 | echo "deploy-container.sh " 30 | echo "" 31 | echo "Arguments:" 32 | echo " version: 'test', 'patch', 'minor', or 'major'." 33 | } 34 | 35 | VERSION=$1 36 | if [[ $VERSION == "" ]]; then 37 | printusage 38 | exit 1 39 | elif [[ ! ($VERSION == "test" || \ 40 | $VERSION == "patch" || \ 41 | $VERSION == "minor" || \ 42 | $VERSION == "major") ]]; then 43 | printusage 44 | exit 1 45 | fi 46 | 47 | echo "Checking for commands..." 48 | trap "echo 'Missing tee.'; exit 1" ERR 49 | which tee &> /dev/null 50 | trap - ERR 51 | 52 | trap "echo 'Missing gcloud.'; exit 1" ERR 53 | which gcloud &> /dev/null 54 | trap - ERR 55 | 56 | trap "echo 'Missing npm.'; exit 1" ERR 57 | which npm &> /dev/null 58 | trap - ERR 59 | 60 | trap "echo 'Missing sed.'; exit 1" ERR 61 | which sed &> /dev/null 62 | trap - ERR 63 | 64 | trap "echo 'Missing awk.'; exit 1" ERR 65 | which awk &> /dev/null 66 | trap - ERR 67 | 68 | trap "echo 'Missing vim.'; exit 1" ERR 69 | which vim &> /dev/null 70 | trap - ERR 71 | 72 | trap "echo 'Missing cat.'; exit 1" ERR 73 | which cat &> /dev/null 74 | trap - ERR 75 | 76 | trap "echo 'Missing jq.'; exit 1" ERR 77 | which jq &> /dev/null 78 | trap - ERR 79 | 80 | # Install all dependencies. 81 | npm install 82 | # Build all output files before deploying container. 83 | 84 | trap "echo 'Failed to create the bundle.'; exit 1" ERR 85 | npm run bundle 86 | trap - ERR 87 | 88 | # Skip for non-production builds. 89 | if [[ $VERSION != "test" ]]; then 90 | # Create new version of hosted UI. 91 | echo "Making a $VERSION version..." 92 | npm version $VERSION 93 | UI_VERSION=$(jq -r ".version" package.json) 94 | echo "Made a $VERSION version: $UI_VERSION." 95 | # Substitute version in file. 96 | trap "echo 'Failed to update container version in auth-server.js'; exit 1" ERR 97 | chmod 755 dist/server/auth-server.js 98 | 99 | case "$OSTYPE" in 100 | darwin*|bsd*) 101 | echo "Using BSD sed style" 102 | sed_no_backup=( -i '' ) 103 | ;; 104 | *) 105 | echo "Using GNU sed style" 106 | sed_no_backup=( -i ) 107 | ;; 108 | esac 109 | 110 | sed -i ${sed_no_backup[@]} "s/${VERSION_PLACEHOLDER}/${UI_VERSION}/g" dist/server/auth-server.js 111 | trap - ERR 112 | fi 113 | 114 | # Set expected GCP project where the container image lives. 115 | gcloud config set project $PROJECT_ID 116 | # Skip for non-production builds. 117 | if [[ $VERSION != "test" ]]; then 118 | # Document changes introduced in the current build. 119 | read -p "Click enter to provide the changes introduced in this version: " next 120 | CHANGELOG_FILE=$(mktemp) 121 | vim $CHANGELOG_FILE 122 | change_summary=$(cat ${CHANGELOG_FILE}) 123 | fi 124 | 125 | # Update cloudbuild.json file with GCR path 126 | tmp=$(mktemp) 127 | # Update path in the steps attribute in cloudbuild.json 128 | jq --arg gcr_path "$GCR_PATH" '.steps[0].args[2] = $gcr_path' cloudbuild.json > "$tmp" && mv "$tmp" cloudbuild.json 129 | # Update path in the images attribute in cloudbuild.json 130 | jq --arg gcr_path "$GCR_PATH" '.images[0] = $gcr_path' cloudbuild.json > "$tmp" && mv "$tmp" cloudbuild.json 131 | 132 | # Containerize the authui app. Save output to temporary file. 133 | OUTPUT_FILE=$(mktemp) 134 | # Note that the authui container image should be made public. 135 | # Cloud Console -> Container Registry -> Settings 136 | gcloud builds submit --config cloudbuild.json ./ | tee "${OUTPUT_FILE}" 137 | # Find new image hash. This is needed to keep track of the current image for the current build. 138 | GCR_VERSION=$(grep "latest: digest: sha256:" "${OUTPUT_FILE}" | awk '{ print $3 }') 139 | echo "Deployed gcr.io/${PROJECT_ID}/${IMG_NAME}@${GCR_VERSION}" 140 | 141 | #Generate SBOM explicitly via command as auto generation is not supported when build is triggered via CLI 142 | gcloud artifacts sbom export --uri=$GCR_PATH 143 | 144 | # Clean up. 145 | rm "${OUTPUT_FILE}" 146 | # Skip for non-production builds. 147 | if [[ $VERSION != "test" ]]; then 148 | rm "${CHANGELOG_FILE}" 149 | # Log changes. 150 | echo "" >> CHANGELOG.md 151 | echo "#v${UI_VERSION}" >> CHANGELOG.md 152 | echo "" >> CHANGELOG.md 153 | echo "gcr.io/${PROJECT_ID}/${IMG_NAME}@${GCR_VERSION}" >> CHANGELOG.md 154 | echo "" >> CHANGELOG.md 155 | echo "${change_summary}" >> CHANGELOG.md 156 | fi 157 | -------------------------------------------------------------------------------- /authui-container/karma.config.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright 2020 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | const path = require('path'); 18 | 19 | module.exports = function(config) { 20 | config.set({ 21 | frameworks: ['mocha', 'chai', 'sinon', 'karma-typescript'], 22 | files: [ 23 | 'src/**/*.ts', 24 | 'common/**/*.ts', 25 | 'src/utils/**/*.ts', 26 | 'test/unit/src/**/*.ts', 27 | 'test/unit/src/utils/**/*.ts', 28 | ], 29 | exclude: [], 30 | preprocessors: { 31 | 'test/unit/src/**/*.ts': ['karma-typescript'], 32 | 'src/utils/**/*.ts': ['karma-typescript', 'coverage'], 33 | 'src/**/*.ts': ['karma-typescript', 'coverage'], 34 | 'common/**/*.ts': ['karma-typescript'], 35 | }, 36 | mime: { 37 | 'text/x-typescript': ['ts'] 38 | }, 39 | karmaTypescriptConfig: { 40 | transformPath: function(path) { 41 | return path.replace(/\.ts$/, '.js'); 42 | }, 43 | tsconfig: './tsconfig.webpack.json', 44 | bundlerOptions: { 45 | transforms: [require('karma-typescript-es6-transform')({ 46 | presets: [ 47 | [ 48 | "@babel/preset-env", 49 | { 50 | "targets": { 51 | "esmodules": true 52 | } 53 | } 54 | ] 55 | ] 56 | })], 57 | }, 58 | }, 59 | reporters: ['verbose', 'progress', 'coverage', 'karma-typescript'], 60 | client: { 61 | clearContext: false 62 | }, 63 | browserConsoleLogOptions: { 64 | level: 'log', 65 | terminal: true 66 | }, 67 | singleRun: true, 68 | port: 9876, 69 | colors: true, 70 | logLevel: config.LOG_INFO, 71 | browsers: ['ChromeHeadless'], 72 | autoWatch: false, 73 | concurrency: Infinity, 74 | coverageReporter: { 75 | includeAllSources: true, 76 | dir: 'coverage/', 77 | reporters: [ 78 | {type: 'html', subdir: 'html'}, 79 | {type: 'text-summary'} 80 | ] 81 | } 82 | }) 83 | }; -------------------------------------------------------------------------------- /authui-container/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "knative-serving-iap-gcip-authui", 3 | "version": "1.0.5", 4 | "description": "Containerized Authentication UI for IAP external identities", 5 | "main": "dist/app.js", 6 | "scripts": { 7 | "bundle": "webpack --config webpack.config.js && tsc -p tsconfig.server.json && mkdir -p dist/public && cp public/*.js dist/public", 8 | "start": "node dist/server/app.js", 9 | "lint": "run-p lint:server lint:client lint:unit lint:common", 10 | "lint:common": "tslint --format stylish -p tsconfig.webpack.json common/*.ts", 11 | "lint:server": "tslint --format stylish -p tsconfig.server.json server/*.ts server/**/*.ts", 12 | "lint:client": "tslint --format stylish -p tsconfig.webpack.json src/*.ts src/**/*.ts", 13 | "lint:unit": "tslint -c tslint-test.json --format stylish test/unit/*.ts test/unit/**/*.ts test/unit/**/**/*.ts", 14 | "test:server": "mocha test/unit/common/*.spec.ts test/unit/server/*.spec.ts test/unit/server/*/*.spec.ts --timeout 20000 --require ts-node/register", 15 | "test:client": "karma start karma.config.js", 16 | "test:unit": "npm run test:server && npm run test:client", 17 | "test": "run-s lint test:unit" 18 | }, 19 | "author": "Google", 20 | "license": "Apache-2.0", 21 | "homepage": "https://github.com/GoogleCloudPlatform/iap-gcip-web-toolkit", 22 | "dependencies": { 23 | "express": "^4.17.1", 24 | "http-proxy-middleware": "^2.0.7", 25 | "request": "^2.88.2", 26 | "request-promise": "^4.2.6" 27 | }, 28 | "devDependencies": { 29 | "@babel/cli": "^7.10.5", 30 | "@babel/core": "^7.10.5", 31 | "@babel/plugin-proposal-class-properties": "^7.10.4", 32 | "@babel/polyfill": "^7.10.5", 33 | "@babel/preset-env": "^7.10.4", 34 | "@babel/register": "^7.10.5", 35 | "@types/bootstrap": "^5.2.10", 36 | "@types/chai": "^4.2.11", 37 | "@types/chai-as-promised": "^7.1.3", 38 | "@types/codemirror": "^0.0.97", 39 | "@types/express": "^4.17.7", 40 | "@types/glob": "^8.1.0", 41 | "@types/jquery": "^3.5.0", 42 | "@types/lodash": "^4.14.158", 43 | "@types/minimist": "^1.2.0", 44 | "@types/mocha": "^8.0.0", 45 | "@types/nock": "^11.1.0", 46 | "@types/node": "^22.7.3", 47 | "@types/request-promise": "^4.1.46", 48 | "@types/sinon": "^9.0.4", 49 | "@types/sinon-chai": "^3.2.4", 50 | "@types/supertest": "6.0.2", 51 | "babel-loader": "^8.1.0", 52 | "body-parser": "^1.19.0", 53 | "bootstrap": "^5.3.3", 54 | "chai": "^4.2.0", 55 | "chai-as-promised": "^7.1.1", 56 | "chromedriver": "latest", 57 | "codemirror": "^5.56.0", 58 | "css-loader": "^7.1.2", 59 | "es6-map": "^0.1.5", 60 | "file-loader": "^6.0.0", 61 | "firebaseui": "^6.0.2", 62 | "gcip-iap": "2.0.0", 63 | "firebase": "^9.8.3", 64 | "handlebars": "^4.7.6", 65 | "jquery": "^3.5.1", 66 | "karma": "^6.3.4", 67 | "karma-chai": "^0.1.0", 68 | "karma-chrome-launcher": "^3.1.0", 69 | "karma-cli": "^2.0.0", 70 | "karma-coverage": "^2.0.2", 71 | "karma-mocha": "^2.0.1", 72 | "karma-sinon": "^1.0.5", 73 | "karma-typescript": "^5.0.3", 74 | "karma-typescript-es6-transform": "^5.0.3", 75 | "karma-typescript-preprocessor": "^0.4.0", 76 | "karma-verbose-reporter": "^0.0.6", 77 | "lodash": "^4.17.19", 78 | "minimist": "^1.2.5", 79 | "mocha": "^10.2.0", 80 | "nock": "^13.0.2", 81 | "npm-run-all": "^4.1.5", 82 | "object.entries": "^1.1.2", 83 | "object.values": "^1.1.1", 84 | "popper.js": "^1.16.1", 85 | "promise-polyfill": "^8.1.3", 86 | "request": "^2.88.2", 87 | "request-promise": "^4.2.6", 88 | "semistandard": "^16.0.1", 89 | "sinon": "^9.0.2", 90 | "sinon-chai": "^3.5.0", 91 | "string-replace-loader": "^3.1.0", 92 | "style-loader": "^1.2.1", 93 | "supertest": "^4.0.2", 94 | "ts-loader": "^8.0.1", 95 | "ts-node": "^8.10.2", 96 | "tslint": "^6.1.2", 97 | "typescript": "^4.9.5", 98 | "url-loader": "^4.1.0", 99 | "url-polyfill": "^1.1.10", 100 | "webpack": "^5.75.0", 101 | "webpack-cli": "^4.9.1", 102 | "whatwg-fetch": "^3.2.0" 103 | }, 104 | "engines": { 105 | "node": ">=10" 106 | }, 107 | "overrides": { 108 | "tough-cookie": "4.1.3", 109 | "cookie": "0.7.0" 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /authui-container/public/style.css: -------------------------------------------------------------------------------- 1 | /** Override FirebaseUI styles. */ 2 | @keyframes fadeIn { 3 | 0% { 4 | opacity: 0; 5 | } 6 | 90% { 7 | opacity: 0; 8 | } 9 | 100% { 10 | opacity: 1; 11 | } 12 | } 13 | 14 | .firebaseui-id-page-spinner { 15 | animation: 3s fadeIn; 16 | } 17 | 18 | .firebaseui-container { 19 | box-shadow: none; 20 | } 21 | /** End of overrides. */ 22 | 23 | body { 24 | background-color: #eee; 25 | margin: 50; 26 | } 27 | 28 | .main-container { 29 | max-width: 400px; 30 | margin: 0 auto; 31 | box-shadow: 0 2px 2px 0 rgba(0,0,0,.14), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.12); 32 | background-color: #fff; 33 | border: 1px solid #cccccc; 34 | padding: 20px; 35 | } 36 | 37 | #admin-container { 38 | max-width: 700px; 39 | margin: 0 auto; 40 | box-shadow: 0 2px 2px 0 rgba(0,0,0,.14), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.12); 41 | background-color: #fff; 42 | border: 1px solid #cccccc; 43 | padding: 20px 20px 5px 20px; 44 | } 45 | 46 | .padded-div { 47 | padding: 10px; 48 | } 49 | 50 | .heading-center { 51 | color: #3b5998; 52 | text-align: center; 53 | justify-content: center; 54 | } 55 | 56 | .hidden { 57 | display: none; 58 | } 59 | 60 | /** https://stackoverflow.com/questions/23584120/line-before-and-after-title-over-image */ 61 | h3.line-through { 62 | color: #777777; 63 | font-size: 1.0rem; 64 | font-weight: 400; 65 | margin: .7em auto; 66 | overflow: hidden; 67 | text-align: center; 68 | width: 100%; 69 | } 70 | 71 | /** https://stackoverflow.com/questions/2812770/add-centered-text-to-the-middle-of-a-hr-like-line */ 72 | h3.line-through:before, h3.line-through:after { 73 | border-bottom: 1px solid rgba(0,0,0,.125); 74 | content: ""; 75 | display: inline-block; 76 | margin: 0 .5em 0 -55%; 77 | vertical-align: middle; 78 | width: 50%; 79 | } 80 | 81 | h3.line-through:after { 82 | margin: 0 -55% 0 .5em; 83 | } 84 | 85 | .blend { 86 | background-color: #eee; 87 | box-shadow: none; 88 | border: 0; 89 | } 90 | 91 | .separator { 92 | display: flex; 93 | align-items: center; 94 | text-align: center; 95 | } 96 | 97 | .separator::before, .separator::after { 98 | content: ''; 99 | flex: 1; 100 | border-bottom: 1px solid #ccc; 101 | } 102 | 103 | .separator::before { 104 | margin-right: .25em; 105 | } 106 | 107 | .separator::after { 108 | margin-left: .25em; 109 | } 110 | 111 | /** Alert styles. */ 112 | .toast-container { 113 | position: absolute; 114 | right: 0; 115 | top: 0; 116 | z-index: 10000; 117 | width: 250px; 118 | } 119 | 120 | .toast-body { 121 | word-wrap: break-word; 122 | } 123 | 124 | /***************/ 125 | .CodeMirror { 126 | border: 1px solid #000000; 127 | } 128 | 129 | .copy-btn { 130 | cursor: pointer; 131 | display: flex; 132 | padding: 5px; 133 | position:absolute; 134 | right: 15px; 135 | top: 5px; 136 | width: 35px; 137 | z-index:1000; 138 | } 139 | 140 | .copy-btn:hover { 141 | background-color: #E6E6E6; 142 | border-radius: 5px; 143 | } 144 | 145 | .spinner-border { 146 | height: 3rem; 147 | width: 3rem; 148 | } 149 | 150 | #loading-spinner { 151 | margin-bottom: 50px; 152 | margin-top: 50px; 153 | } 154 | -------------------------------------------------------------------------------- /authui-container/server/api/authenticated-request-handler.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the 10 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | * express or implied. See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | import {AccessTokenManager} from './token-manager'; 16 | import { deepCopy } from '../../common/deep-copy'; 17 | import { 18 | HttpServerRequestHandler, BaseRequestOptions, RequestOptions, 19 | HttpResponse, 20 | } from '../../server/utils/http-server-request-handler'; 21 | 22 | /** Utility for sending authenticated server side HTTP requests. */ 23 | export class AuthenticatedRequestHandler extends HttpServerRequestHandler { 24 | /** 25 | * Instantiates an authenticated request handler instance using the access token manager 26 | * provided. 27 | * @param baseOptions The base options for the request. 28 | * @param accessTokenManager The access token manager used to facilitate retrieval of OAuth access tokens. 29 | * @param logger The optional logging function used to log request information for debugging purposes. 30 | * This can be accessed via Cloud Run LOGS tab. 31 | */ 32 | constructor( 33 | baseOptions: BaseRequestOptions, 34 | private readonly accessTokenManager: AccessTokenManager, 35 | logger?: (...args: any[]) => void) { 36 | super(baseOptions, logger); 37 | } 38 | 39 | /** 40 | * Sends the specified request options to the underlying endpoint while injecting an 41 | * OAuth access token in the request header. 42 | * @param requestOptions The variable request options to append to base config options. 43 | * @param defaultMessage The default error message if none is available in the response. 44 | * @return A promise that resolves with the full response. 45 | */ 46 | send( 47 | requestOptions?: RequestOptions | null, 48 | defaultMessage?: string): Promise { 49 | const modifiedRequestOptions = deepCopy(requestOptions || {}); 50 | if (typeof modifiedRequestOptions.headers === 'undefined') { 51 | modifiedRequestOptions.headers = {}; 52 | } 53 | // Get OAuth access token and add to header. 54 | return this.accessTokenManager.getAccessToken() 55 | .then((accessToken) => { 56 | // Inject access token to request. 57 | modifiedRequestOptions.headers.Authorization = `Bearer ${accessToken}`; 58 | return super.send(modifiedRequestOptions, defaultMessage); 59 | }); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /authui-container/server/api/metadata-server.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the 10 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | * express or implied. See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | import {HttpServerRequestHandler} from '../../server/utils/http-server-request-handler'; 16 | import {TokenManager, AccessTokenManager} from './token-manager'; 17 | 18 | /** Metadata server project number endpoint. */ 19 | const METADATA_SERVER_PROJECT_NUMBER_URL = 20 | 'http://metadata.google.internal/computeMetadata/v1/project/numeric-project-id'; 21 | /** Metadata server project ID endpoint. */ 22 | const METADATA_SERVER_PROJECT_ID_URL = 23 | 'http://metadata.google.internal/computeMetadata/v1/project/project-id'; 24 | /** Metadata server zone endpoint. */ 25 | const METADATA_SERVER_ZONE_URL = 26 | 'http://metadata.google.internal/computeMetadata/v1/instance/zone'; 27 | /** Network request timeout duration. */ 28 | const TIMEOUT_DURATION = 10000; 29 | /** Default zone to use when it is not determined for whatever reason. */ 30 | export const DEFAULT_ZONE = 'US-CENTRAL1'; 31 | /** Default error message to show when project ID fails to be determined. */ 32 | export const DEFAULT_ERROR_MESSAGE_PROJECT_ID = 'Unable to retrieve the project ID.'; 33 | /** Default error message to show when project number fails to be determined. */ 34 | export const DEFAULT_ERROR_MESSAGE_PROJECT_NUMBER = 'Unable to retrieve the project number.'; 35 | /** Default error message to show when GCP zone fails to be determined. */ 36 | export const DEFAULT_ERROR_MESSAGE_ZONE = 'Unable to retrieve the GCP zone.'; 37 | 38 | /** Interface defining all application related data. */ 39 | export interface ApplicationData { 40 | getProjectId(): Promise; 41 | getProjectNumber(): Promise; 42 | getZone(): Promise; 43 | log(...args: any[]): void; 44 | } 45 | 46 | /** 47 | * Metadata server APIs for retrieving OAuth access tokens, project ID, 48 | * numeric project ID, current GCP zone, etc. 49 | */ 50 | export class MetadataServer implements AccessTokenManager, ApplicationData { 51 | private readonly tokenManager: TokenManager; 52 | private readonly projectIdRetriever: HttpServerRequestHandler; 53 | private readonly projectNumberRetriever: HttpServerRequestHandler; 54 | private readonly zoneRetriever: HttpServerRequestHandler; 55 | private projectId: string; 56 | private projectNumber: string; 57 | private zone: string; 58 | 59 | /** 60 | * Instantiates an instance of the metadata server APIs handler. 61 | * @param scopes The OAuth scopes to set on the generated access tokens. 62 | * @param logger The optional logging function used to log request information for debugging purposes. 63 | * This can be accessed via Cloud Run LOGS tab. 64 | */ 65 | constructor(scopes?: string[], private readonly logger?: (...args: any[]) => void) { 66 | this.tokenManager = new TokenManager(scopes); 67 | this.projectIdRetriever = new HttpServerRequestHandler({ 68 | method: 'GET', 69 | url: METADATA_SERVER_PROJECT_ID_URL, 70 | headers: { 71 | 'Metadata-Flavor': 'Google', 72 | }, 73 | timeout: TIMEOUT_DURATION, 74 | }, logger); 75 | this.projectNumberRetriever = new HttpServerRequestHandler({ 76 | method: 'GET', 77 | url: METADATA_SERVER_PROJECT_NUMBER_URL, 78 | headers: { 79 | 'Metadata-Flavor': 'Google', 80 | }, 81 | timeout: TIMEOUT_DURATION, 82 | }, logger); 83 | this.zoneRetriever = new HttpServerRequestHandler({ 84 | method: 'GET', 85 | url: METADATA_SERVER_ZONE_URL, 86 | headers: { 87 | 'Metadata-Flavor': 'Google', 88 | }, 89 | timeout: TIMEOUT_DURATION, 90 | }, logger); 91 | } 92 | 93 | /** 94 | * Used to log underlying operations for debugging purposes, if a logger is available. 95 | * @param args The list of arguments to log. 96 | */ 97 | log(...args: any[]) { 98 | if (this.logger) { 99 | this.logger(...args); 100 | } 101 | } 102 | 103 | /** 104 | * @return A promise that resolves with a Google OAuth access token. 105 | * A cached token is returned if it is not yet expired. 106 | */ 107 | getAccessToken(forceRefresh: boolean = false): Promise { 108 | return this.tokenManager.getAccessToken(forceRefresh) 109 | .then((result) => { 110 | return result.access_token; 111 | }) 112 | .catch((error) => { 113 | // For access token getter, only log errors. 114 | this.log('Error encountered while getting Metadata server access token', error); 115 | throw error; 116 | }); 117 | } 118 | 119 | /** @return A promise that resolves with the project ID. */ 120 | getProjectId(): Promise { 121 | if (this.projectId) { 122 | return Promise.resolve(this.projectId); 123 | } else { 124 | return this.projectIdRetriever.send(null, DEFAULT_ERROR_MESSAGE_PROJECT_ID) 125 | .then((httpResponse) => { 126 | if (httpResponse.statusCode === 200 && 127 | httpResponse.body) { 128 | this.projectId = httpResponse.body as string; 129 | return this.projectId; 130 | } else { 131 | // No data in body. 132 | throw new Error(DEFAULT_ERROR_MESSAGE_PROJECT_ID); 133 | } 134 | }); 135 | } 136 | } 137 | 138 | /** @return A promise that resolves with the project number. */ 139 | getProjectNumber(): Promise { 140 | if (this.projectNumber) { 141 | return Promise.resolve(this.projectNumber); 142 | } else { 143 | return this.projectNumberRetriever.send(null, DEFAULT_ERROR_MESSAGE_PROJECT_NUMBER) 144 | .then((httpResponse) => { 145 | if (httpResponse.statusCode === 200 && 146 | httpResponse.body) { 147 | this.projectNumber = httpResponse.body.toString(); 148 | return this.projectNumber; 149 | } else { 150 | // No data in body. 151 | throw new Error(DEFAULT_ERROR_MESSAGE_PROJECT_NUMBER); 152 | } 153 | }); 154 | } 155 | } 156 | 157 | /** @return A promise that resolves with the zone. */ 158 | getZone(): Promise { 159 | if (this.zone) { 160 | return Promise.resolve(this.zone); 161 | } else { 162 | return this.zoneRetriever.send(null, DEFAULT_ERROR_MESSAGE_ZONE) 163 | .then((httpResponse) => { 164 | if (httpResponse.statusCode === 200 && 165 | httpResponse.body) { 166 | const zoneName: string = httpResponse.body; 167 | // Format: projects/327715512941/zones/us-central1-1 168 | const matches = zoneName.match(/\/zones\/(.*)\-[a-zA-Z1-9]$/); 169 | this.zone = matches && matches.length > 1 ? matches[1] : DEFAULT_ZONE; 170 | return this.zone; 171 | } else { 172 | // No data in body. 173 | throw new Error(DEFAULT_ERROR_MESSAGE_ZONE); 174 | } 175 | }); 176 | } 177 | } 178 | } -------------------------------------------------------------------------------- /authui-container/server/api/token-manager.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the 10 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | * express or implied. See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | import {HttpServerRequestHandler} from '../../server/utils/http-server-request-handler'; 16 | 17 | /** Interface defining a Google OAuth access token. */ 18 | export interface GoogleOAuthAccessToken { 19 | access_token: string; 20 | expires_in: number; 21 | } 22 | 23 | /** Interface defining an OAuth access token manager used to retrieve tokens. */ 24 | export interface AccessTokenManager { 25 | getAccessToken(): Promise; 26 | } 27 | 28 | /** Interface defining a credential object used to retrieve access tokens. */ 29 | export interface Credential { 30 | getAccessToken(): Promise; 31 | } 32 | 33 | /** Metadata server access token endpoint. */ 34 | const METADATA_SERVER_ACCESS_TOKEN_URL = 35 | 'http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token'; 36 | /** The default OAuth scope to include in the access token. */ 37 | const DEFAULT_OAUTH_SCOPE = 'https://www.googleapis.com/auth/cloud-platform'; 38 | /** Time offset in milliseconds for forcing a refresh before a token expires. */ 39 | export const OFFSET = 30000; 40 | /** Network request timeout duration. */ 41 | const TIMEOUT_DURATION = 10000; 42 | /** Default error message to show when access token fails to be obtained. */ 43 | const DEFAULT_ERROR_MESSAGE = 'Unable to retrieve an OAuth access tokens.'; 44 | 45 | /** Utility used to manage OAuth access tokens generated via the metadata server. */ 46 | export class TokenManager implements Credential { 47 | private readonly metadataServerTokenRetriever: HttpServerRequestHandler; 48 | private expirationTime: number; 49 | private accessToken: string | null; 50 | 51 | /** 52 | * Instantiates an instance of a token manager used to retrieve OAuth access 53 | * tokens retrieved from the metadata server. 54 | * @param scopes The OAuth scopes to set on the generated access tokens. 55 | */ 56 | constructor(scopes: string[] = [DEFAULT_OAUTH_SCOPE]) { 57 | this.metadataServerTokenRetriever = new HttpServerRequestHandler({ 58 | method: 'GET', 59 | url: `${METADATA_SERVER_ACCESS_TOKEN_URL}?scopes=${scopes.join(',')}`, 60 | headers: { 61 | 'Metadata-Flavor': 'Google', 62 | }, 63 | timeout: TIMEOUT_DURATION, 64 | }); 65 | } 66 | 67 | /** 68 | * @return A promise that resolves with a Google OAuth access token. 69 | * A cached token is returned if it is not yet expired. 70 | */ 71 | getAccessToken(forceRefresh: boolean = false): Promise { 72 | const currentTime = new Date().getTime(); 73 | if (!forceRefresh && 74 | (this.accessToken && 75 | currentTime + OFFSET <= this.expirationTime)) { 76 | return Promise.resolve({ 77 | access_token: this.accessToken, 78 | expires_in: (this.expirationTime - currentTime) / 1000, 79 | }); 80 | } 81 | return this.metadataServerTokenRetriever.send(null, DEFAULT_ERROR_MESSAGE) 82 | .then((httpResponse) => { 83 | if (httpResponse.statusCode === 200 && httpResponse.body) { 84 | const tokenResponse: GoogleOAuthAccessToken = typeof httpResponse.body === 'object' ? 85 | httpResponse.body : JSON.parse(httpResponse.body); 86 | this.accessToken = tokenResponse.access_token; 87 | this.expirationTime = currentTime + (tokenResponse.expires_in * 1000); 88 | return tokenResponse; 89 | } else { 90 | throw new Error(DEFAULT_ERROR_MESSAGE); 91 | } 92 | }); 93 | } 94 | 95 | /** Reset cached access tokens. */ 96 | reset() { 97 | this.accessToken = null; 98 | } 99 | } -------------------------------------------------------------------------------- /authui-container/server/app.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the 10 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | * express or implied. See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | import express = require('express'); 16 | import {AuthServer} from './auth-server'; 17 | 18 | const app = express(); 19 | const authServer = new AuthServer(app); 20 | authServer.start(process.env.PORT); 21 | 22 | export = authServer.server; -------------------------------------------------------------------------------- /authui-container/server/templates.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the 10 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | * express or implied. See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | import handlebars = require('handlebars'); 16 | 17 | /** Main template used for handling sign-in with IAP. */ 18 | const main = handlebars.compile(` 19 | 20 | 21 | 22 | 23 | 24 |
25 |
26 | 27 |
28 |
29 |
30 |

31 | 32 |

33 | 36 |
37 |
38 | 39 | 40 | `); 41 | 42 | /** 43 | * Main template used for handling administrative functionality for customizing the 44 | * Auth UI configuration. 45 | */ 46 | const admin = handlebars.compile(` 47 | 48 | 49 | 50 | 51 | 52 | 53 |
54 |
55 | 56 |
57 |
58 |
59 |
60 |
61 | Success 62 | 65 |
66 |
67 |
68 |
69 | 84 | 85 | 86 | `); 87 | 88 | export { 89 | main, 90 | admin, 91 | }; 92 | -------------------------------------------------------------------------------- /authui-container/server/utils/error.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the 10 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | * express or implied. See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | export interface ErrorResponse { 16 | error: { 17 | code: number; 18 | message: string; 19 | status?: string; 20 | }; 21 | } 22 | 23 | export const ERROR_MAP: {[key: string]: ErrorResponse} = { 24 | INVALID_ARGUMENT: { 25 | error: { 26 | code: 400, 27 | message: 'Client specified an invalid argument.', 28 | status: 'INVALID_ARGUMENT', 29 | }, 30 | }, 31 | FAILED_PRECONDITION: { 32 | error: { 33 | code: 400, 34 | message: 'Request can not be executed in the current system state.', 35 | status: 'FAILED_PRECONDITION', 36 | }, 37 | }, 38 | OUT_OF_RANGE: { 39 | error: { 40 | code: 400, 41 | message: 'Client specified an invalid range.', 42 | status: 'OUT_OF_RANGE', 43 | }, 44 | }, 45 | UNAUTHENTICATED: { 46 | error: { 47 | code: 401, 48 | message: 'Request not authenticated due to missing, invalid, or expired OAuth token.', 49 | status: 'UNAUTHENTICATED', 50 | }, 51 | }, 52 | PERMISSION_DENIED: { 53 | error: { 54 | code: 403, 55 | message: 'Client does not have sufficient permission.', 56 | status: 'PERMISSION_DENIED', 57 | }, 58 | }, 59 | NOT_FOUND: { 60 | error: { 61 | code: 404, 62 | message: 'A specified resource is not found, or the request is rejected by undisclosed reasons.', 63 | status: 'NOT_FOUND', 64 | }, 65 | }, 66 | ABORTED: { 67 | error: { 68 | code: 409, 69 | message: 'Concurrency conflict, such as read-modify-write conflict.', 70 | status: 'ABORTED', 71 | }, 72 | }, 73 | ALREADY_EXISTS: { 74 | error: { 75 | code: 409, 76 | message: 'The resource that a client tried to create already exists.', 77 | status: 'ALREADY_EXISTS', 78 | }, 79 | }, 80 | RESOURCE_EXHAUSTED: { 81 | error: { 82 | code: 429, 83 | message: 'Either out of resource quota or reaching rate limiting.', 84 | status: 'RESOURCE_EXHAUSTED', 85 | }, 86 | }, 87 | CANCELLED: { 88 | error: { 89 | code: 499, 90 | message: 'Request cancelled by the client.', 91 | status: 'CANCELLED', 92 | }, 93 | }, 94 | DATA_LOSS: { 95 | error: { 96 | code: 500, 97 | message: 'Unrecoverable data loss or data corruption. ', 98 | status: 'DATA_LOSS', 99 | }, 100 | }, 101 | UNKNOWN: { 102 | error: { 103 | code: 500, 104 | message: 'Unknown server error.', 105 | status: 'UNKNOWN', 106 | }, 107 | }, 108 | INTERNAL: { 109 | error: { 110 | code: 500, 111 | message: 'Internal server error.', 112 | status: 'INTERNAL', 113 | }, 114 | }, 115 | NOT_IMPLEMENTED: { 116 | error: { 117 | code: 501, 118 | message: 'API method not implemented by the server.', 119 | status: 'NOT_IMPLEMENTED', 120 | }, 121 | }, 122 | UNAVAILABLE: { 123 | error: { 124 | code: 503, 125 | message: 'Service unavailable.', 126 | status: 'UNAVAILABLE', 127 | }, 128 | }, 129 | DEADLINE_EXCEEDED: { 130 | error: { 131 | code: 504, 132 | message: 'Request deadline exceeded.', 133 | status: 'DEADLINE_EXCEEDED', 134 | }, 135 | }, 136 | }; 137 | -------------------------------------------------------------------------------- /authui-container/src/admin.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the 10 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | * express or implied. See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | import '../node_modules/bootstrap/dist/css/bootstrap.min.css'; 16 | import '../node_modules/firebaseui/dist/firebaseui.css'; 17 | import '../public/style.css'; 18 | import './polyfill'; 19 | import $ = require('jquery'); 20 | (window as any).$ = (window as any).jQuery = $; 21 | 22 | import { AdminUi } from './admin-ui'; 23 | import { onDomReady } from './utils/index'; 24 | 25 | // The query selector where the admin UI will be rendered. 26 | const UI_ELEMENT_SELECTOR = '#admin-container'; 27 | 28 | // When document is ready, initialize and render the AdminUi. 29 | onDomReady(document) 30 | .then(() => { 31 | $('.toast').toast('hide'); 32 | const ui = new AdminUi(UI_ELEMENT_SELECTOR, () => { 33 | $('.toast').toast('show'); 34 | }); 35 | return ui.render(); 36 | }); -------------------------------------------------------------------------------- /authui-container/src/polyfill.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the 10 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | * express or implied. See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | // Polyfills for IE 11. 16 | // tslint:disable-next-line:no-var-requires 17 | require('@babel/polyfill'); 18 | // tslint:disable-next-line:no-var-requires 19 | const entries = require('object.entries'); 20 | // tslint:disable-next-line:no-var-requires 21 | const values = require('object.values'); 22 | 23 | // from:https://github.com/jserz/js_piece/blob/master/DOM/ChildNode/remove()/remove().md 24 | ((arr) => { 25 | arr.forEach((item) => { 26 | if (item.hasOwnProperty('remove')) { 27 | return; 28 | } 29 | Object.defineProperty(item, 'remove', { 30 | configurable: true, 31 | enumerable: true, 32 | writable: true, 33 | value: function remove() { 34 | this.parentNode.removeChild(this); 35 | } 36 | }); 37 | }); 38 | })([Element.prototype, CharacterData.prototype, DocumentType.prototype]); 39 | 40 | // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/includes 41 | if (!String.prototype.includes) { 42 | String.prototype.includes = function(search: any, start = 0) { 43 | 'use strict'; 44 | 45 | if (search instanceof RegExp) { 46 | throw TypeError('first argument must not be a RegExp'); 47 | } 48 | return this.indexOf(search, start) !== -1; 49 | }; 50 | } 51 | 52 | (() => { 53 | // https://stackoverflow.com/questions/19345392/why-arent-my-parameters-getting-passed-through-to-a-dispatched-event/ 54 | if ((window.document as any).documentMode) { 55 | function CustomEvent(event: any, params: any) { 56 | params = params || {bubbles: false, cancelable: false, detail: undefined}; 57 | const evt = document.createEvent('CustomEvent'); 58 | evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail); 59 | return evt; 60 | } 61 | 62 | CustomEvent.prototype = (window as any).Event.prototype; 63 | 64 | (window as any).CustomEvent = CustomEvent; 65 | 66 | // Map.prototype.values() not provided in IE11. 67 | // tslint:disable-next-line 68 | window.Map = require('es6-map/polyfill'); 69 | } 70 | })(); 71 | 72 | if (!(Object as any).values) { 73 | values.shim(); 74 | } 75 | 76 | if (!(Object as any).entries) { 77 | entries.shim(); 78 | } 79 | -------------------------------------------------------------------------------- /authui-container/src/script.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the 10 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | * express or implied. See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | import '../node_modules/bootstrap/dist/css/bootstrap.min.css'; 16 | import '../node_modules/firebaseui/dist/firebaseui.css'; 17 | import '../public/style.css'; 18 | import './polyfill'; 19 | 20 | import { SignInUi } from './sign-in-ui'; 21 | import { onDomReady } from './utils/index'; 22 | 23 | // The query selector where then sign-in UI will be rendered. 24 | const UI_ELEMENT_SELECTOR = '#firebaseui-container'; 25 | 26 | // When document is ready, initialize and render the SignInUi. 27 | onDomReady(document) 28 | .then(() => { 29 | const ui = new SignInUi(UI_ELEMENT_SELECTOR); 30 | return ui.render(); 31 | }); 32 | -------------------------------------------------------------------------------- /authui-container/src/utils/FirebaseWrapper.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the 10 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | * express or implied. See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | // tslint:disable-next-line:no-submodule-imports 16 | import { initializeApp } from 'firebase/app'; 17 | // tslint:disable-next-line:no-submodule-imports 18 | import { getAuth, getRedirectResult, reauthenticateWithPopup, signInWithRedirect } from 'firebase/auth'; 19 | import { getCredentialFromResult } from './index'; 20 | // This wrapper is primarily used in order to stub these methods during testing with sinon 21 | // Sinon does not allow stubbing standalone functions. See https://github.com/sinonjs/sinon/issues/562 22 | const firebaseWrapper = { 23 | _initializeApp: ((config) => { 24 | return initializeApp(config) 25 | }), 26 | 27 | _getAuth: ((app) => { 28 | return getAuth(app); 29 | }), 30 | _getRedirectResult: ((auth) => { 31 | return getRedirectResult(auth); 32 | }), 33 | _getCredentialFromResult: ((auth) => { 34 | return getCredentialFromResult(auth) 35 | }), 36 | _reauthenticateWithPopup: ((user, provider) => { 37 | return reauthenticateWithPopup(user, provider); 38 | }), 39 | _signInWithRedirect: ((auth, provider) => { 40 | return signInWithRedirect(auth, provider); 41 | }) 42 | }; 43 | 44 | export default firebaseWrapper; -------------------------------------------------------------------------------- /authui-container/src/utils/browser.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * Enums for Browser name. 19 | */ 20 | export enum BrowserName { 21 | Android = 'Android', 22 | Blackberry = 'Blackberry', 23 | Edge = 'Edge', 24 | Firefox = 'Firefox', 25 | IE = 'IE', 26 | IEMobile = 'IEMobile', 27 | Opera = 'Opera', 28 | Other = 'Other', 29 | Chrome = 'Chrome', 30 | Safari = 'Safari', 31 | Silk = 'Silk', 32 | Webos = 'Webos', 33 | } 34 | 35 | /** 36 | * @param userAgent The navigator user agent string. 37 | * @return The browser name, eg Safari, Firefox, etc. 38 | */ 39 | export function getBrowserName(userAgent?: string): string { 40 | const ua = (userAgent || getUserAgentString()).toLowerCase(); 41 | if (ua.indexOf('opera/') !== -1 || 42 | ua.indexOf('opr/') !== -1 || 43 | ua.indexOf('opios/') !== -1) { 44 | return BrowserName.Opera; 45 | } else if (ua.indexOf('iemobile') !== -1) { 46 | // Windows phone IEMobile browser. 47 | return BrowserName.IEMobile; 48 | } else if (ua.indexOf('msie') !== -1 || 49 | ua.indexOf('trident/') !== -1) { 50 | return BrowserName.IE; 51 | } else if (ua.indexOf('edge/') !== -1) { 52 | return BrowserName.Edge; 53 | } else if (ua.indexOf('firefox/') !== -1) { 54 | return BrowserName.Firefox; 55 | } else if (ua.indexOf('silk/') !== -1) { 56 | return BrowserName.Silk; 57 | } else if (ua.indexOf('blackberry') !== -1) { 58 | // Blackberry browser. 59 | return BrowserName.Blackberry; 60 | } else if (ua.indexOf('webos') !== -1) { 61 | // WebOS default browser. 62 | return BrowserName.Webos; 63 | } else if (ua.indexOf('safari/') !== -1 && 64 | ua.indexOf('chrome/') === -1 && 65 | ua.indexOf('crios/') === -1 && 66 | ua.indexOf('android') === -1) { 67 | return BrowserName.Safari; 68 | } else if ((ua.indexOf('chrome/') !== -1 || 69 | ua.indexOf('crios/') !== -1) && 70 | ua.indexOf('edge/') === -1) { 71 | return BrowserName.Chrome; 72 | } else if (ua.indexOf('android') !== -1) { 73 | // Android stock browser. 74 | return BrowserName.Android; 75 | } else { 76 | // Most modern browsers have name/version at end of user agent string. 77 | const re = new RegExp('([a-zA-Z\\d\\.]+)\/[a-zA-Z\\d\\.]*$'); 78 | const matches = userAgent.match(re); 79 | if (matches && matches.length === 2) { 80 | return matches[1]; 81 | } 82 | } 83 | return BrowserName.Other; 84 | } 85 | 86 | /** 87 | * @return The user agent string reported by the environment, or the 88 | * empty string if not available. 89 | */ 90 | function getUserAgentString(): string { 91 | return (window && window.navigator && window.navigator.userAgent) || ''; 92 | } 93 | 94 | /** 95 | * Detects whether browser is running on a mobile device. 96 | * 97 | * @param userAgent The navigator user agent. 98 | * @return True if the browser is running on a mobile device. 99 | */ 100 | export function isMobileBrowser(userAgent?: string): boolean { 101 | const ua = userAgent || getUserAgentString(); 102 | const uaLower = ua.toLowerCase(); 103 | if (uaLower.match(/android/) || 104 | uaLower.match(/webos/) || 105 | uaLower.match(/iphone|ipad|ipod/) || 106 | uaLower.match(/blackberry/) || 107 | uaLower.match(/windows phone/) || 108 | uaLower.match(/iemobile/)) { 109 | return true; 110 | } 111 | return false; 112 | } 113 | -------------------------------------------------------------------------------- /authui-container/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the 10 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | * express or implied. See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | import {isNonNullObject} from './validator'; 16 | // tslint:disable-next-line:no-submodule-imports 17 | import { UserCredential, GoogleAuthProvider } from 'firebase/auth'; 18 | /** 19 | * Defines a new read-only property directly on an object and returns the object. 20 | * 21 | * @param obj The object on which to define the property. 22 | * @param prop The name of the property to be defined or modified. 23 | * @param value The value associated with the property. 24 | */ 25 | export function addReadonlyGetter(obj: object, prop: string, value: any): void { 26 | Object.defineProperty(obj, prop, { 27 | value, 28 | // Make this property read-only. 29 | writable: false, 30 | // Include this property during enumeration of obj's properties. 31 | enumerable: true, 32 | }); 33 | } 34 | 35 | /** 36 | * Removes entries in an object whose values are undefined and returns the same 37 | * object. This only removes the top-level undefined fields. 38 | * 39 | * @param obj The object whose undefined fields are to be removed. 40 | * @return The same object with undefined fields removed. 41 | */ 42 | export function removeUndefinedFields(obj: T): T { 43 | // If obj is not a non-null object, return it back. 44 | if (!isNonNullObject(obj)) { 45 | return obj; 46 | } 47 | for (const key in obj) { 48 | if (typeof obj[key] === 'undefined') { 49 | delete obj[key]; 50 | } 51 | } 52 | return obj; 53 | } 54 | 55 | /** 56 | * Returns a promise that resolves on DOM readiness. 57 | * 58 | * @param doc The document reference. 59 | * @return A promise that resolves when DOM is ready. 60 | */ 61 | export function onDomReady(doc: Document): Promise { 62 | return new Promise((resolve) => { 63 | if (doc.readyState !== 'loading') { 64 | resolve(); 65 | } else { 66 | doc.addEventListener('DOMContentLoaded', (event) => { 67 | resolve(); 68 | }); 69 | } 70 | }); 71 | } 72 | 73 | /** 74 | * Sets the provided CSS URL dynamically to the current document. 75 | * @param doc The HTML document reference. 76 | * @param cssUrl The CSS URL to dynamically include. 77 | */ 78 | export function setStyleSheet(doc: Document, cssUrl: string) { 79 | const head = doc.getElementsByTagName('head')[0]; 80 | const link = doc.createElement('link'); 81 | link.rel = 'stylesheet'; 82 | link.type = 'text/css'; 83 | link.href = cssUrl; 84 | link.media = 'all'; 85 | head.appendChild(link); 86 | } 87 | 88 | 89 | /** 90 | * Returns a deep copy of an object or array. 91 | * 92 | * @param value The object or array to deep copy. 93 | * @return A deep copy of the provided object or array. 94 | */ 95 | export function deepCopy(value: T): T { 96 | return deepExtend(undefined, value); 97 | } 98 | 99 | /** 100 | * Checks for deep equality between the provided parameters. 101 | * https://stackoverflow.com/questions/25456013/javascript-deepequal-comparison/25456134 102 | * 103 | * @param a The first object to be compared for deep equality. 104 | * @param b The second object to be compared for deep equality. 105 | * @return Whether a deep equals b. 106 | */ 107 | export function deepEqual(a: any, b: any): boolean { 108 | if (a === b) { 109 | return true; 110 | } else if (typeof a === 'object' && 111 | typeof b === 'object' && 112 | a !== null && 113 | b !== null && 114 | Object.keys(a).length === Object.keys(b).length) { 115 | // Match properties one by one. 116 | for (const prop in a) { 117 | if (a.hasOwnProperty(prop)) { 118 | if (!b.hasOwnProperty(prop) || 119 | !deepEqual(a[prop], b[prop])) { 120 | return false; 121 | } 122 | } 123 | } 124 | // All sub properties match. 125 | return true; 126 | } 127 | return false; 128 | } 129 | 130 | 131 | /** 132 | * Copies properties from source to target (recursively allows extension of objects and arrays). 133 | * Scalar values in the target are over-written. If target is undefined, an object of the 134 | * appropriate type will be created (and returned). 135 | * 136 | * We recursively copy all child properties of plain objects in the source - so that namespace-like 137 | * objects are merged. 138 | * 139 | * Note that the target can be a function, in which case the properties in the source object are 140 | * copied onto it as static properties of the function. 141 | * 142 | * @param target The value which is being extended. 143 | * @param source The value whose properties are extending the target. 144 | * @return The target value. 145 | */ 146 | export function deepExtend(target: any, source: any): any { 147 | if (!(source instanceof Object)) { 148 | return source; 149 | } 150 | switch (source.constructor) { 151 | case Date: 152 | // Treat Dates like scalars; if the target date object had any child 153 | // properties - they will be lost! 154 | const dateValue = (source as any) as Date; 155 | return new Date(dateValue.getTime()); 156 | 157 | case Object: 158 | if (target === undefined) { 159 | target = {}; 160 | } 161 | break; 162 | 163 | case Array: 164 | // Always copy the array source and overwrite the target. 165 | target = []; 166 | break; 167 | 168 | default: 169 | // Not a plain Object - treat it as a scalar. 170 | return source; 171 | } 172 | 173 | for (const prop in source) { 174 | if (!source.hasOwnProperty(prop)) { 175 | continue; 176 | } 177 | target[prop] = deepExtend(target[prop], source[prop]); 178 | } 179 | 180 | return target; 181 | } 182 | 183 | /** 184 | * Selects the content within a text area and copies it to clipboard. 185 | * @param textAreaElement The text area to select. 186 | */ 187 | export function copyTextAreaContent(textAreaElement: HTMLTextAreaElement) { 188 | textAreaElement.select(); 189 | document.execCommand('copy'); 190 | } 191 | 192 | /** 193 | * Returns the OAuthCredential from the provided authentication result, if not null. 194 | * Returns null otherwise. 195 | * @param result The authentication result to extract the credential from. 196 | */ 197 | export function getCredentialFromResult(result: UserCredential){ 198 | let credential = null; 199 | if (result){ 200 | credential = GoogleAuthProvider.credentialFromResult(result); 201 | } 202 | return credential; 203 | } -------------------------------------------------------------------------------- /authui-container/src/utils/validator.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright 2020 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * Validates that a value is an array. 19 | * 20 | * @param value The value to validate. 21 | * @return Whether the value is an array or not. 22 | */ 23 | export function isArray(value: any): boolean { 24 | return Array.isArray(value); 25 | } 26 | 27 | /** 28 | * Validates that a value is a non-empty array. 29 | * 30 | * @param value The value to validate. 31 | * @return Whether the value is a non-empty array or not. 32 | */ 33 | export function isNonEmptyArray(value: any): boolean { 34 | return isArray(value) && value.length !== 0; 35 | } 36 | 37 | /** 38 | * Validates that a value is a boolean. 39 | * 40 | * @param value The value to validate. 41 | * @return Whether the value is a boolean or not. 42 | */ 43 | export function isBoolean(value: any): boolean { 44 | return typeof value === 'boolean'; 45 | } 46 | 47 | /** 48 | * Validates that a value is a number. 49 | * 50 | * @param value The value to validate. 51 | * @return Whether the value is a number or not. 52 | */ 53 | export function isNumber(value: any): boolean { 54 | return typeof value === 'number' && !isNaN(value); 55 | } 56 | 57 | /** 58 | * Validates that a value is a string. 59 | * 60 | * @param value The value to validate. 61 | * @return Whether the value is a string or not. 62 | */ 63 | export function isString(value: any): value is string { 64 | return typeof value === 'string'; 65 | } 66 | 67 | /** 68 | * Validates that a value is a non-empty string. 69 | * 70 | * @param value The value to validate. 71 | * @return Whether the value is a non-empty string or not. 72 | */ 73 | export function isNonEmptyString(value: any): value is string { 74 | return isString(value) && value !== ''; 75 | } 76 | 77 | /** 78 | * Validates that a value is a nullable object. 79 | * 80 | * @param value The value to validate. 81 | * @return Whether the value is an object or not. 82 | */ 83 | export function isObject(value: any): boolean { 84 | return typeof value === 'object' && !isArray(value); 85 | } 86 | 87 | /** 88 | * Validates that a value is a non-null object. 89 | * 90 | * @param value The value to validate. 91 | * @return Whether the value is a non-null object or not. 92 | */ 93 | export function isNonNullObject(value: any): boolean { 94 | return isObject(value) && value !== null; 95 | } 96 | -------------------------------------------------------------------------------- /authui-container/test/unit/server/app.spec.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright 2020 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import * as chai from 'chai'; 18 | import * as sinon from 'sinon'; 19 | import * as sinonChai from 'sinon-chai'; 20 | import * as chaiAsPromised from 'chai-as-promised'; 21 | import {AuthServer} from '../../../server/auth-server'; 22 | 23 | chai.should(); 24 | chai.use(sinonChai); 25 | chai.use(chaiAsPromised); 26 | 27 | const expect = chai.expect; 28 | 29 | describe('app server', () => { 30 | let server; 31 | let currentPort: string; 32 | let startAuthServerSpy: sinon.SinonSpy; 33 | let stopAuthServerSpy: sinon.SinonSpy; 34 | // Test with less frequently used port number. 35 | const TEST_PORT = '5238'; 36 | 37 | beforeEach(() => { 38 | // Save environment variable port if available. 39 | currentPort = process.env.PORT; 40 | startAuthServerSpy = sinon.spy(AuthServer.prototype, 'start'); 41 | stopAuthServerSpy = sinon.spy(AuthServer.prototype, 'stop'); 42 | }); 43 | 44 | afterEach(() => { 45 | // Restore port if available. 46 | process.env.PORT = currentPort; 47 | if (server) { 48 | server.close(); 49 | } 50 | startAuthServerSpy.restore(); 51 | stopAuthServerSpy.restore(); 52 | }); 53 | 54 | it('should initialize an AuthServer and use process.env.PORT for server port', () => { 55 | process.env.PORT = TEST_PORT; 56 | server = require('../../../server/app'); 57 | expect(startAuthServerSpy).to.have.been.calledOnce.and.calledWith(TEST_PORT); 58 | expect(startAuthServerSpy.getCall(0).thisValue).to.be.instanceof(AuthServer); 59 | expect(startAuthServerSpy.getCall(0).thisValue.server).to.be.equal(server); 60 | expect(stopAuthServerSpy).to.not.have.been.called; 61 | }) 62 | }); 63 | -------------------------------------------------------------------------------- /authui-container/test/unit/src/utils/browser.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import * as _ from 'lodash'; 18 | import {expect} from 'chai'; 19 | import { 20 | getBrowserName, isMobileBrowser, BrowserName, 21 | } from '../../../../src/utils/browser'; 22 | import { USER_AGENTS } from '../test-utils'; 23 | 24 | describe('getBrowserName', () => { 25 | it('should return Edge for edge userAgent', () => { 26 | expect(getBrowserName(USER_AGENTS.edge)).to.equal(BrowserName.Edge); 27 | }); 28 | 29 | it('should return Firefox for firefox userAgent', () => { 30 | expect(getBrowserName(USER_AGENTS.firefox)).to.equal(BrowserName.Firefox); 31 | }); 32 | 33 | it('should return Silk for silk userAgent', () => { 34 | expect(getBrowserName(USER_AGENTS.silk)).to.equal(BrowserName.Silk); 35 | }); 36 | 37 | it('should return Safari for Safari userAgent', () => { 38 | expect(getBrowserName(USER_AGENTS.safari)).to.equal(BrowserName.Safari); 39 | }); 40 | 41 | it('should return Chrome for Chrome userAgent', () => { 42 | expect(getBrowserName(USER_AGENTS.chrome)).to.equal(BrowserName.Chrome); 43 | }); 44 | 45 | it('should return Android for Android stock userAgent', () => { 46 | expect(getBrowserName(USER_AGENTS.android)).to.equal(BrowserName.Android); 47 | }); 48 | 49 | it('should return Blackberry for Blackberry userAgent', () => { 50 | expect(getBrowserName(USER_AGENTS.blackberry)).to.equal(BrowserName.Blackberry); 51 | }); 52 | 53 | it('should return IEMobile for windows phone userAgent', () => { 54 | expect(getBrowserName(USER_AGENTS.windowsPhone)).to.equal(BrowserName.IEMobile); 55 | }); 56 | 57 | it('should return Webos for WebOS userAgent', () => { 58 | expect(getBrowserName(USER_AGENTS.webOS)).to.equal(BrowserName.Webos); 59 | }); 60 | 61 | it('should return custom browser name for recognizable userAgent', () => { 62 | const ua = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like ' + 63 | 'Gecko) Awesome/2.0.012'; 64 | expect(getBrowserName(ua)).to.equal('Awesome'); 65 | }); 66 | 67 | it('should return Other for unrecognizable userAgent', () => { 68 | const ua = 'Mozilla/5.0 (iPhone; CPU iPhone OS 8_2 like Mac OS X) AppleWebKi' + 69 | 't/600.1.4 (KHTML, like Gecko) Mobile/12D508 [FBAN/FBIOS;FBAV/27.0.0.1' + 70 | '0.12;FBBV/8291884;FBDV/iPhone7,1;FBMD/iPhone;FBSN/iPhone OS;FBSV/8.2;' + 71 | 'FBSS/3; FBCR/vodafoneIE;FBID/phone;FBLC/en_US;FBOP/5]'; 72 | expect(getBrowserName(ua)).to.equal(BrowserName.Other); 73 | }); 74 | }); 75 | 76 | describe('isMobileBrowser', () => { 77 | const mobileUserAgents = [ 78 | USER_AGENTS.iOS7iPod, 79 | USER_AGENTS.iOS7iPhone, 80 | USER_AGENTS.iOS7iPad, 81 | USER_AGENTS.iOS9iPhone, 82 | USER_AGENTS.iOS8iPhone, 83 | USER_AGENTS.android, 84 | USER_AGENTS.blackberry, 85 | USER_AGENTS.webOS, 86 | USER_AGENTS.windowsPhone, 87 | USER_AGENTS.chrios, 88 | ]; 89 | const desktopUserAgents = [ 90 | USER_AGENTS.firefox, 91 | USER_AGENTS.opera, 92 | USER_AGENTS.ie, 93 | USER_AGENTS.edge, 94 | USER_AGENTS.silk, 95 | USER_AGENTS.safari, 96 | USER_AGENTS.chrome, 97 | ]; 98 | 99 | it('should return true for mobile browser', () => { 100 | mobileUserAgents.forEach((ua: string) => { 101 | expect(isMobileBrowser(ua)).to.be.true; 102 | }); 103 | }); 104 | 105 | it('should return false for desktop browser', () => { 106 | desktopUserAgents.forEach((ua: string) => { 107 | expect(isMobileBrowser(ua)).to.be.false; 108 | }); 109 | }); 110 | }); 111 | -------------------------------------------------------------------------------- /authui-container/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "es2015", "es2015.promise"], 4 | "baseUrl": ".", 5 | "paths": { 6 | "*": [ 7 | "node_modules/*" 8 | ] 9 | } 10 | }, 11 | "include": [ 12 | "server/**/*", 13 | "server/api/**/*", 14 | "src/**/*", 15 | "common/**/*" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /authui-container/tsconfig.server.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "esModuleInterop": true, 6 | "target": "es6", 7 | "noImplicitAny": true, 8 | "moduleResolution": "node", 9 | "outDir": "dist", 10 | "baseUrl": ".", 11 | "paths": { 12 | "*": [ 13 | "node_modules/*" 14 | ] 15 | } 16 | }, 17 | "exclude": [ 18 | "src/**/*" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /authui-container/tsconfig.webpack.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "target": "es5", 5 | "noImplicitAny": false, 6 | "sourceMap": true, 7 | "lib": ["dom", "es2015", "es2015.promise"], 8 | "outDir": "lib", 9 | "rootDir": "." 10 | }, 11 | "include": [ 12 | "src/**/*", 13 | "src/utils/**/*", 14 | "common/**/*", 15 | "test/unit/src/**/*" 16 | ], 17 | "exclude": [ 18 | "node_modules" 19 | ] 20 | } -------------------------------------------------------------------------------- /authui-container/tslint-test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:latest", 3 | "rules": { 4 | "interface-name": false, 5 | "member-ordering": [ 6 | true, 7 | { 8 | "order": [ 9 | { 10 | "name": "public static field", 11 | "kinds": [ 12 | "public-static-field", 13 | "protected-static-field" 14 | ] 15 | }, 16 | { 17 | "name": "private static field", 18 | "kinds": [ 19 | "private-static-field" 20 | ] 21 | }, 22 | { 23 | "name": "public instance field", 24 | "kinds": [ 25 | "public-instance-field", 26 | "protected-instance-field" 27 | ] 28 | }, 29 | { 30 | "name": "private instance field", 31 | "kinds": [ 32 | "private-instance-field" 33 | ] 34 | }, 35 | { 36 | "name": "public static method", 37 | "kinds": [ 38 | "public-static-method", 39 | "protected-static-method", 40 | "public-static-accessor", 41 | "protected-static-accessor" 42 | ] 43 | }, 44 | { 45 | "name": "private static method", 46 | "kinds": [ 47 | "private-static-method", 48 | "private-static-accessor" 49 | ] 50 | }, 51 | { 52 | "name": "public instance method", 53 | "kinds": [ 54 | "public-constructor", 55 | "protected-constructor", 56 | "public-instance-method", 57 | "protected-instance-method", 58 | "public-instance-accessor", 59 | "protected-instance-accessor" 60 | ] 61 | }, 62 | { 63 | "name": "private instance method", 64 | "kinds": [ 65 | "private-constructor", 66 | "private-instance-method", 67 | "private-instance-accessor" 68 | ] 69 | } 70 | ] 71 | } 72 | ], 73 | "prefer-object-spread": false, 74 | "no-implicit-dependencies": [true, "dev", "optional"], 75 | "max-classes-per-file": false, 76 | "max-line-length": [true, 120], 77 | "no-consecutive-blank-lines": false, 78 | "object-literal-sort-keys": false, 79 | "ordered-imports": false, 80 | "quotemark": [true, "single", "avoid-escape"], 81 | "no-unused-expression": false, 82 | "variable-name": [true, "ban-keywords", "check-format", "allow-trailing-underscore"] 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /authui-container/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:latest", 3 | "rules": { 4 | "interface-name": false, 5 | "member-ordering": [ 6 | true, 7 | { 8 | "order": [ 9 | { 10 | "name": "public static field", 11 | "kinds": [ 12 | "public-static-field", 13 | "protected-static-field" 14 | ] 15 | }, 16 | { 17 | "name": "private static field", 18 | "kinds": [ 19 | "private-static-field" 20 | ] 21 | }, 22 | { 23 | "name": "public instance field", 24 | "kinds": [ 25 | "public-instance-field", 26 | "protected-instance-field" 27 | ] 28 | }, 29 | { 30 | "name": "private instance field", 31 | "kinds": [ 32 | "private-instance-field" 33 | ] 34 | }, 35 | { 36 | "name": "public static method", 37 | "kinds": [ 38 | "public-static-method", 39 | "protected-static-method", 40 | "public-static-accessor", 41 | "protected-static-accessor" 42 | ] 43 | }, 44 | { 45 | "name": "private static method", 46 | "kinds": [ 47 | "private-static-method", 48 | "private-static-accessor" 49 | ] 50 | }, 51 | { 52 | "name": "public instance method", 53 | "kinds": [ 54 | "public-constructor", 55 | "protected-constructor", 56 | "public-instance-method", 57 | "protected-instance-method", 58 | "public-instance-accessor", 59 | "protected-instance-accessor" 60 | ] 61 | }, 62 | { 63 | "name": "private instance method", 64 | "kinds": [ 65 | "private-constructor", 66 | "private-instance-method", 67 | "private-instance-accessor" 68 | ] 69 | } 70 | ] 71 | } 72 | ], 73 | "prefer-object-spread": false, 74 | "no-implicit-dependencies": [true, "dev", "optional"], 75 | "max-classes-per-file": false, 76 | "max-line-length": [true, 120], 77 | "no-consecutive-blank-lines": false, 78 | "object-literal-sort-keys": false, 79 | "ordered-imports": false, 80 | "quotemark": [true, "single", "avoid-escape"], 81 | "variable-name": [true, "ban-keywords", "check-format", "allow-trailing-underscore"] 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /authui-container/webpack.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2020 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for t`he specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 'use strict'; 17 | 18 | const path = require('path'); 19 | const pkg = require('./package.json'); 20 | 21 | const config = { 22 | context: __dirname, 23 | entry: { 24 | // JS script for sign-in page. 25 | 'script': './src/script.ts', 26 | // JS script for admin page. 27 | 'admin': './src/admin.ts', 28 | }, 29 | output: { 30 | filename: '[name].js', 31 | path: path.resolve(__dirname, './public'), 32 | }, 33 | resolve: { 34 | extensions: ['.js', '.ts'], 35 | alias: { 36 | 'promise-polyfill': path.resolve(__dirname, './node_modules/promise-polyfill/'), 37 | 'url-polyfill': path.resolve(__dirname, './node_modules/url-polyfill/'), 38 | 'whatwg-fetch': path.resolve(__dirname, './node_modules/whatwg-fetch/'), 39 | }, 40 | }, 41 | stats: { 42 | colors: true, 43 | reasons: true, 44 | chunks: true, 45 | }, 46 | module: { 47 | rules: [ 48 | { 49 | test: /\.js$/, 50 | use: [ 51 | { 52 | loader: 'babel-loader' 53 | } 54 | ], 55 | exclude: /node_modules/ 56 | }, 57 | { 58 | test: /\.ts$/, 59 | use: [ 60 | { 61 | loader: 'ts-loader' 62 | } 63 | ], 64 | exclude: /node_modules/ 65 | }, 66 | { 67 | test: /\.css$/, 68 | use: ['style-loader', 'css-loader'] 69 | }, 70 | { 71 | test: /\.(png|woff|woff2|eot|ttf|svg)$/, 72 | use: [ 73 | { 74 | loader: 'url-loader', 75 | options: { 76 | limit: 100000 77 | } 78 | } 79 | ] 80 | }, 81 | { 82 | // Inject hosted UI version into gcip-iap. 83 | // This is useful for detecting traffic from older versions of the hosted UI. 84 | test: /\.ts$/, 85 | use: [ 86 | { 87 | loader: 'string-replace-loader', 88 | options: { 89 | search: '__XXX_HOSTED_UI_VERSION_XXX__', 90 | replace: 'ui-' + pkg.version, 91 | flags: 'g' 92 | } 93 | } 94 | ] 95 | } 96 | ] 97 | }, 98 | mode: 'none', 99 | optimization: { 100 | minimize: true 101 | } 102 | } 103 | 104 | module.exports = config; 105 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gcip-iap", 3 | "version": "2.0.0", 4 | "description": "GCIP/IAP JS SDK", 5 | "author": "Google", 6 | "license": "Apache-2.0", 7 | "homepage": "https://github.com/GoogleCloudPlatform/iap-gcip-web-toolkit", 8 | "engines": { 9 | "node": ">=8.0.0" 10 | }, 11 | "keywords": [ 12 | "cloud", 13 | "identity", 14 | "platform", 15 | "gcip", 16 | "iap", 17 | "firebase", 18 | "proxy", 19 | "aware", 20 | "authentication" 21 | ], 22 | "repository": { 23 | "type": "git", 24 | "url": "https://github.com/GoogleCloudPlatform/iap-gcip-web-toolkit", 25 | "sshclone": "git@github.com:GoogleCloudPlatform/iap-gcip-web-toolkit.git" 26 | }, 27 | "publishConfig": { 28 | "registry": "https://wombat-dressing-room.appspot.com" 29 | }, 30 | "main": "dist/index.cjs.js", 31 | "module": "dist/index.esm.js", 32 | "files": [ 33 | "dist/", 34 | "LICENSE", 35 | "README.md", 36 | "package.json" 37 | ], 38 | "types": "dist/index.d.ts", 39 | "dependencies": { 40 | "@types/node": "^8.10.51", 41 | "node-forge": "^1.3.1" 42 | }, 43 | "peerDependencies": { 44 | "firebase": "^9.8.3", 45 | "promise-polyfill": "^8.1.0", 46 | "url-polyfill": "^1.1.3", 47 | "whatwg-fetch": "^3.0.0" 48 | }, 49 | "devDependencies": { 50 | "@firebase/auth-types": "0.10.2", 51 | "@types/chai": "^4.2.15", 52 | "@types/chai-as-promised": "^7.1.3", 53 | "@types/lodash": "^4.14.137", 54 | "@types/mocha": "^2.2.48", 55 | "@types/nock": "^9.1.0", 56 | "@types/sinon": "^4.1.3", 57 | "@types/sinon-chai": "^2.7.36", 58 | "chai": "^4.3.4", 59 | "chai-as-promised": "^7.1.1", 60 | "chalk": "^1.1.3", 61 | "chromedriver": "latest", 62 | "del": "^2.2.1", 63 | "dtslint": "^4.2.1", 64 | "firebase": "^9.1.3", 65 | "firebase-admin": "^11.5.0", 66 | "firebase-tools": "^9.21.0", 67 | "gulp": "^4.0.2", 68 | "gulp-exit": "0.0.2", 69 | "gulp-gzip": "^1.4.2", 70 | "gulp-header": "^1.8.8", 71 | "gulp-replace": "^0.5.4", 72 | "gulp-tar": "^2.1.0", 73 | "gulp-tslint": "^8.1.4", 74 | "gulp-typescript": "^6.0.0-alpha.1", 75 | "karma": "6.2.0", 76 | "karma-chai": "0.1.0", 77 | "karma-chrome-launcher": "3.1.0", 78 | "karma-cli": "2.0.0", 79 | "karma-coverage": "2.0.3", 80 | "karma-mocha": "2.0.1", 81 | "karma-sinon": "1.0.5", 82 | "karma-typescript": "5.5.0", 83 | "karma-typescript-es6-transform": "5.5.0", 84 | "karma-typescript-preprocessor": "0.4.0", 85 | "karma-verbose-reporter": "0.0.6", 86 | "lodash": "^4.17.19", 87 | "merge2": "^1.2.4", 88 | "minimist": "^1.2.5", 89 | "mocha": "^8.1.3", 90 | "nock": "^9.1.8", 91 | "npm-run-all": "^4.1.2", 92 | "promise-polyfill": "^8.1.0", 93 | "request": "^2.88.0", 94 | "rollup": "1.4.0", 95 | "rollup-plugin-babel-minify": "^6.2.0", 96 | "rollup-plugin-commonjs": "^9.2.0", 97 | "rollup-plugin-node-resolve": "^4.0.0", 98 | "rollup-plugin-typescript2": "^0.12.0", 99 | "selenium-webdriver": "4.0.0-alpha.7", 100 | "sinon": "^4.4.5", 101 | "sinon-chai": "^2.8.0", 102 | "ts-node": "^3.3.0", 103 | "tslint": "^5.9.0", 104 | "typescript": "^3.9.7", 105 | "url-polyfill": "^1.1.7", 106 | "webpack": "4.43.0", 107 | "whatwg-fetch": "^3.0.0", 108 | "yargs": "15.4.1" 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /package/gcip-cloud-functions/README.md: -------------------------------------------------------------------------------- 1 | Please refer to Identity Platform Blocking functions [documentation](https://cloud.google.com/identity-platform/docs/blocking-functions) 2 | -------------------------------------------------------------------------------- /sample/app/app.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2019, Google, Inc. 2 | # Licensed under the Apache License, Version 2.0 (the "License"); 3 | # you may not use this file except in compliance with the License. 4 | # You may obtain a copy of the License at 5 | # 6 | # http://www.apache.org/licenses/LICENSE-2.0 7 | # 8 | # Unless required by applicable law or agreed to in writing, software 9 | # distributed under the License is distributed on an "AS IS" BASIS, 10 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | # See the License for the specific language governing permissions and 12 | # limitations under the License. 13 | 14 | runtime: nodejs 15 | env: flex 16 | 17 | # This sample incurs costs to run on the App Engine flexible environment. 18 | # The settings below are to reduce costs during testing and are not appropriate 19 | # for production use. For more information, see: 20 | # https://cloud.google.com/appengine/docs/flexible/nodejs/configuring-your-app-with-app-yaml 21 | manual_scaling: 22 | instances: 1 23 | resources: 24 | cpu: 1 25 | memory_gb: 0.5 26 | disk_size_gb: 10 27 | -------------------------------------------------------------------------------- /sample/app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "iap-external-identities-gae-app", 3 | "version": "0.0.0", 4 | "description": "GAE app for IAP external identities", 5 | "main": "app.js", 6 | "scripts": { 7 | "deploy": "gcloud app deploy", 8 | "start": "node server/app.js" 9 | }, 10 | "author": "Google", 11 | "license": "Apache-2.0", 12 | "dependencies": { 13 | "body-parser": "^1.15.2", 14 | "express": "^4.16.3", 15 | "handlebars": "^4.7.7", 16 | "jsonwebtoken": "^9.0.0", 17 | "request": "^2.88.0" 18 | }, 19 | "engines": { 20 | "node": ">=10" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /sample/app/server/app.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the 10 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | * express or implied. See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | const request = require('request'); 16 | const IapJwtVerifier = require('./verify-iap-jwt'); 17 | const express = require('express'); 18 | const app = express(); 19 | const bodyParser = require('body-parser'); 20 | const templates = require('./templates'); 21 | const path = require('path'); 22 | 23 | // Useful links: 24 | // https://cloud.google.com/iap/docs/external-identities 25 | // https://cloud.google.com/iap/docs/signed-headers-howto 26 | // https://cloud.google.com/appengine/docs/standard/nodejs/quickstart 27 | 28 | app.enable('trust proxy'); 29 | 30 | /** Metadata service url for getting project number. */ 31 | const PROJECT_NUMBER_URL = 'http://metadata.google.internal/computeMetadata/v1/project/numeric-project-id'; 32 | /** Injected IAP JWT header name. */ 33 | const IAP_JWT_HEADER = 'x-goog-iap-jwt-assertion'; 34 | /** IAP JWT verifier. */ 35 | const jwtVerifierPromise = getProjectNumber().then((projectNumber) => { 36 | return new IapJwtVerifier(process.env.GOOGLE_CLOUD_PROJECT, projectNumber); 37 | }); 38 | 39 | /** @return {Promise} A promise that resolves with the project number. */ 40 | function getProjectNumber() { 41 | return new Promise((resolve, reject) => { 42 | request({ 43 | url: PROJECT_NUMBER_URL, 44 | headers: { 45 | 'Metadata-Flavor': 'Google', 46 | }, 47 | json: true 48 | }, (error, response, body) => { 49 | if (error || response.statusCode !== 200) { 50 | reject(error || new Error('Unable to retrieve project number')); 51 | } else { 52 | resolve(body); 53 | } 54 | }); 55 | }); 56 | } 57 | 58 | /** 59 | * Renders the resource profile page and serves it in the response. 60 | * @param {function(!Object): string} template The template generating function. 61 | * @param {!Object} req The expressjs request. 62 | * @param {!Object} res The expressjs response. 63 | * @param {*} decodedClaims The decoded claims from verified IAP JWT. 64 | * @return {!Promise} A promise that resolves on success. 65 | */ 66 | function serveContentForUser(template, req, res, decodedClaims) { 67 | let gcipClaims = decodedClaims.gcip || null; 68 | res.set('Content-Type', 'text/html'); 69 | res.end(template({ 70 | sub: decodedClaims.sub, 71 | email: decodedClaims.email, 72 | emailVerifed: !!(gcipClaims && gcipClaims.email_verified), 73 | photoURL: gcipClaims && gcipClaims.picture, 74 | displayName: (gcipClaims && gcipClaims.name) || 'N/A', 75 | tenandId: (gcipClaims && gcipClaims.firebase && gcipClaims.firebase.tenant) || 'N/A', 76 | gcipClaims: JSON.stringify(gcipClaims, null, 2), 77 | iapClaims: JSON.stringify(decodedClaims, null, 2), 78 | signoutURL: '?gcp-iap-mode=GCIP_SIGNOUT', 79 | switchTenantURL: '?gcp-iap-mode=CLEAR_LOGIN_COOKIE', 80 | sessionRefreshURL: '?gcp-iap-mode=SESSION_REFRESHER', 81 | })); 82 | } 83 | 84 | /** 85 | * Checks if a user is signed in. If not, shows an error message. 86 | * @return {function()} The middleware function to run. 87 | */ 88 | function checkIfSignedIn() { 89 | return (req, res, next) => { 90 | // Allow access only if user is signed in. 91 | const iapToken = req.headers[IAP_JWT_HEADER] || ''; 92 | // Get JWT verifier. 93 | jwtVerifierPromise.then((jwtVerifier) => { 94 | jwtVerifier.verify(iapToken).then((decodedClaims) => { 95 | req.claims = decodedClaims; 96 | next(); 97 | }).catch((error) => { 98 | res.status(503).send('403: Permission denied (' + error.message + ')'); 99 | }); 100 | }); 101 | }; 102 | } 103 | 104 | // Support JSON-encoded bodies. 105 | app.use(bodyParser.json()); 106 | // Support URL-encoded bodies. 107 | app.use(bodyParser.urlencoded({ 108 | extended: true 109 | })); 110 | // Show error message if user is not signed in. 111 | app.use(checkIfSignedIn()); 112 | // Static CSS assets. 113 | app.use('/styles', express.static(path.join(__dirname, '../styles'))); 114 | 115 | app.get('/', (req, res) => { 116 | res.redirect('/resource'); 117 | }); 118 | 119 | /** 120 | * Get the resource endpoint. This will display the current authenticated user's claims. 121 | */ 122 | app.get('/resource', (req, res) => { 123 | // Serve content for signed in user. 124 | return serveContentForUser(templates.main, req, res, req.claims); 125 | }); 126 | 127 | // Start the server. 128 | const PORT = process.env.PORT || 8080; 129 | app.listen(PORT, () => { 130 | console.log(`App listening on port ${PORT}`); 131 | console.log('Press Ctrl+C to quit.'); 132 | }); 133 | -------------------------------------------------------------------------------- /sample/app/server/templates.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the 10 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | * express or implied. See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | const HandleBars = require('handlebars'); 16 | 17 | /** Main template used for resource specific user profile. */ 18 | const main = HandleBars.compile(` 19 | 20 | 21 | 22 | Tenant {{tenandId}} 23 | 24 | 25 |
26 |

Welcome to the IAP External Identities sample app

27 |
28 |
29 | {{#if photoURL}} 30 |
31 | 32 |
33 | {{/if}} 34 |
{{sub}}
35 |
{{displayName}}
36 |
37 | {{email}} ( 38 | {{#if emailVerified}} 39 | Verified 40 | {{else}} 41 | Unverified 42 | {{/if}} 43 | ) 44 |
45 |
{{tenandId}}
46 |
47 |
{{gcipClaims}}
48 |
49 |
50 |
{{iapClaims}}
51 |
52 |
53 |
54 |
55 | 56 | 57 |
58 |
59 |
60 | 61 | 62 | 63 | `); 64 | 65 | module.exports = { 66 | main, 67 | }; 68 | -------------------------------------------------------------------------------- /sample/app/server/verify-iap-jwt.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the 10 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | * express or implied. See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | const jwt = require('jsonwebtoken'); 16 | const request = require('request'); 17 | 18 | /** IAP signed JWT algorithm. */ 19 | const ALGORITHM = 'ES256'; 20 | /** IAP signed JWT issuer URL. */ 21 | const ISSUER = 'https://cloud.google.com/iap'; 22 | /** IAP public keys URL. */ 23 | const PUBLIC_KEY_URL = 'https://www.gstatic.com/iap/verify/public_key'; 24 | 25 | /** Defines the IAP JWT verifier. */ 26 | class IapJwtVerifier { 27 | /** 28 | * Initializes an IAP JWT verifier. 29 | * @param {string} projectId 30 | * @param {string} projectNumber 31 | * @constructor 32 | */ 33 | constructor(projectId, projectNumber) { 34 | /** @private {string} The project ID. */ 35 | this.projectId = projectId; 36 | /** @private {string} The project number. */ 37 | this.projectNumber = projectNumber; 38 | }; 39 | 40 | /** 41 | * Verifies the IAP JWT. 42 | * @param {string} jwtToken The signed IAP JWT token to verify. 43 | * @return {Promise} The decoded payload of the verified JWT. 44 | */ 45 | verify(jwtToken) { 46 | let header; 47 | let payload; 48 | return Promise.resolve().then(() => { 49 | // For GAE: /projects/PROJECT_NUMBER/apps/PROJECT_ID 50 | const aud = `/projects/${this.projectNumber}/apps/${this.projectId}`; 51 | const fullDecodedToken = jwt.decode(jwtToken, { 52 | complete: true, 53 | }); 54 | header = fullDecodedToken && fullDecodedToken.header; 55 | payload = fullDecodedToken && fullDecodedToken.payload; 56 | 57 | if (!fullDecodedToken) { 58 | throw new Error('Decoding the JWT failed.'); 59 | } else if (typeof header.kid === 'undefined') { 60 | throw new Error('IAP JWT has no "kid" claim.'); 61 | } else if (header.alg !== ALGORITHM) { 62 | throw new Error(`IAP JWT has incorrect algorithm. Expected ${ALGORITHM} algorithm but got ${header.alg}`); 63 | } else if (payload.aud !== aud) { 64 | throw new Error(`IAP JWT has incorrect audience. Expected ${aud} but got ${payload.aud}`); 65 | } else if (payload.iss !== ISSUER) { 66 | throw new Error(`IAP JWT has incorrect issuer. Expected ${ISSUER} algorithm but got ${payload.iss}`); 67 | } else if (typeof payload.sub !== 'string' || !payload.sub) { 68 | throw new Error('IAP JWT has no valid "sub".') 69 | } 70 | return this.fetchPublicKey(header.kid); 71 | }).then((publicKey) => { 72 | return this.verifyJwtSignatureWithKey(jwtToken, publicKey); 73 | }); 74 | } 75 | 76 | /** 77 | * @param {string} The kid whose public cert is to be returned. 78 | * @return {Promise} A promise that resolves with the public key that the provided kid maps to. 79 | * @private 80 | */ 81 | fetchPublicKey(kid) { 82 | if (typeof this.publicKeys !== 'undefined' && 83 | this.publicKeys.hasOwnProperty(kid)) { 84 | return Promise.resolve(this.publicKeys[kid]); 85 | } 86 | return new Promise((resolve, reject) => { 87 | request({ 88 | url: PUBLIC_KEY_URL, 89 | json: true 90 | }, (error, response, body) => { 91 | if (!error && response.statusCode === 200) { 92 | // Cache public keys. 93 | this.publicKeys = body; 94 | if (this.publicKeys.hasOwnProperty(kid)) { 95 | // Return the corresponding key. 96 | resolve(body[kid]); 97 | } else { 98 | reject('IAP JWT has "kid" claim which does not correspond to a known public key.'); 99 | } 100 | } else { 101 | reject(error); 102 | } 103 | }); 104 | }); 105 | } 106 | 107 | /** 108 | * Verifies the IAP token signature using the provided key. 109 | * @param {string} jwtToken The IAP token. 110 | * @param {string} publicKey The corresponding public key. 111 | * @return {Promise} A promise that resolves with the decoded JWT claims. 112 | * @private 113 | */ 114 | verifyJwtSignatureWithKey(jwtToken, publicKey) { 115 | return new Promise((resolve, reject) => { 116 | jwt.verify(jwtToken, publicKey, { 117 | algorithms: [ALGORITHM], 118 | }, (error, decodedToken) => { 119 | if (error) { 120 | if (error.name === 'TokenExpiredError') { 121 | return reject(new Error('IAP JWT is expired')); 122 | } else if (error.name === 'JsonWebTokenError') { 123 | return reject(new Error('IAP JWT has invalid signature')); 124 | } 125 | return reject(new Error(error.message)); 126 | } else { 127 | resolve(decodedToken); 128 | } 129 | }); 130 | }); 131 | } 132 | } 133 | 134 | module.exports = IapJwtVerifier; 135 | -------------------------------------------------------------------------------- /sample/app/styles/styles.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | body { 6 | margin: 0; 7 | } 8 | 9 | #container { 10 | max-width: 700px; 11 | margin: 0 auto; 12 | text-align: center; 13 | } 14 | 15 | .clearfix { 16 | clear: both; 17 | } 18 | 19 | .hidden { 20 | display: none; 21 | } 22 | 23 | #user-info { 24 | border: 1px solid #CCC; 25 | clear: both; 26 | margin: 0 auto 20px; 27 | max-width: 700px; 28 | padding: 10px; 29 | text-align: left; 30 | } 31 | 32 | #photo-container { 33 | background-color: #EEE; 34 | border: 1px solid #CCC; 35 | float: left; 36 | height: 80px; 37 | margin-right: 10px; 38 | width: 80px; 39 | } 40 | 41 | #photo { 42 | height: 80px; 43 | margin: 0; 44 | width: 80px; 45 | } 46 | 47 | .item-on-same-line { 48 | float: left; 49 | padding-right: 20px; 50 | } 51 | 52 | .claims { 53 | overflow: auto; 54 | } 55 | 56 | @media (max-width: 300px) { 57 | #photo-container, 58 | #photo { 59 | height: 40px; 60 | width: 40px; 61 | } 62 | } -------------------------------------------------------------------------------- /sample/authui-firebaseui/README.md: -------------------------------------------------------------------------------- 1 | # Configuring IAP with a Single Federated non-Google Provider Using FirebaseUI 2 | 3 | This sample walks you through deploying an App Engine flexible environment 4 | application and securing it with 5 | [Cloud Identity-Aware Proxy](https://cloud.google.com/iap/docs/external-identities) 6 | (Cloud IAP) external identities, powered by 7 | [Google Cloud Identity Platform](https://cloud.google.com/identity-platform/) 8 | (GCIP). This sample will also work for a GCE or GKE IAP resource. 9 | 10 | More specifically, this quickstart sample demonstrates using a federated provider 11 | like Facebook to sign in to an IAP gated resource (GAE app). The same flow can be 12 | applied for other federated providers (SAML, OIDC, OAuth, etc) with minimal 13 | adjustments to the code. 14 | 15 | This quickstart works for single project level IdP or single tenant level IdP. 16 | 17 | ## Table of Contents 18 | 19 | 1. [Dependencies](#dependencies) 20 | 2. [Prerequisite](#prerequisite) 21 | 3. [Enable GCIP](#enable-gcip) 22 | 4. [Deploy the authentication page](#deploy-the-authentication-page) 23 | 5. [Deploy to App Engine Flexible Environment](#deploy-to-app-engine-flexible-environment) 24 | 6. [Enable IAP](#enable-iap) 25 | 7. [Test Access](#test-access) 26 | 27 | ## Dependencies 28 | 29 | To set up a development environment to build the sample from source, you must 30 | have the following installed: 31 | 32 | - Node.js (>= 8.0.0) 33 | - npm (should be included with Node.js) 34 | 35 | Download the sample application source code and its dependencies with: 36 | 37 | ```bash 38 | git clone https://github.com/GoogleCloudPlatform/iap-gcip-web-toolkit.git 39 | cd iap-gcip-web-toolkit/sample/app 40 | npm install 41 | ``` 42 | 43 | ## Prerequisite 44 | Create your project in the [Cloud Console](https://console.cloud.google.com). 45 | 46 | You will need to create the GAE app, IAP and GCIP projects in the same Google 47 | Cloud project. You cannot use cross project external identities with IAP. 48 | 49 | ## Enable Identity Platform 50 | 51 | In order to use non-Google identities with IAP, Google Cloud Identity Platform 52 | needs to be enabled. For this quickstart, Facebook provider needs to be configured 53 | for this project. Go to the Identity Platform 54 | [Cloud Console](https://console.cloud.google.com/customer-identity/providers/) 55 | page to configure it. 56 | 57 | OAuth callback URL should be set as instructed: 58 | `https://firebase-project-id.firebaseapp.com/__/auth/handler` 59 | 60 | [Multi-tenancy](https://cloud.google.com/identity-platform/docs/multi-tenancy-quickstart) 61 | is not required for this sample. However, the IdP can also be configured on 62 | the tenant too for this quickstart. In that case, only one tenant should be 63 | associated with the IAP resource. 64 | 65 | ## Deploy the authentication page 66 | 67 | Note that it is not a requirement to have the same Firebase Hosting project 68 | as your GCIP project. It is only done here for convenience. Firebase Hosting 69 | is merely used as a static file hosting service here. However, if a different 70 | project is used, the Firebase hosting domain has to be whitelisted in the GCIP 71 | list of authorized domains. 72 | 73 | Install all dependencies for the sample `AuthUI` in `iap-gcip-web-toolkit` repo. 74 | 75 | ```bash 76 | cd sample/authui-firebaseui 77 | npm install 78 | ``` 79 | 80 | (Optional) If you want to use a different provider (eg. 81 | [SAML](https://cloud.google.com/identity-platform/docs/how-to-enable-application-for-saml) 82 | or 83 | [OIDC](https://cloud.google.com/identity-platform/docs/how-to-enable-application-for-oidc)), 84 | change the following line in `sample/authui-firebaseui/src/script.ts`: 85 | 86 | ```javascript 87 | // ... 88 | signInOptions: [ 89 | // Replace Facebook if you want to use a different IdP. 90 | // FacebookAuthProvider.PROVIDER_ID, 91 | { 92 | // Copy provider ID from the Cloud Console. 93 | provider: 'saml.myProvider', // or 'oidc.myProvider', etc. 94 | } 95 | ], 96 | // ... 97 | ``` 98 | 99 | Install the Firebase command line tool with `npm install -g firebase-tools` (See 100 | [docs](https://firebase.google.com/docs/cli/#setup)). 101 | 102 | Deploy the sample app to one of your own Firebase Hosting instance: 103 | 104 | ```bash 105 | firebase use --add 106 | ``` 107 | 108 | Select the project you have created in the prerequisite, and type in `default` or 109 | any other name as the alias to use for this project. 110 | 111 | To deploy the authentication UI to production, in the same directory 112 | `sample/authui-firebaseui`, run: 113 | 114 | ```bash 115 | npm run deploy 116 | ``` 117 | 118 | This will deploy the authentication UI to: 119 | `https://firebase-project-id.firebaseapp.com` 120 | 121 | You can use this URL as your authentication URL when configuring IAP. 122 | 123 | ## Deploy to App Engine Flexible Environment 124 | 125 | Follow the instructions to [deploy your GAE application](../app/README.md). 126 | 127 | ## Enable IAP 128 | 129 | - Go to the 130 | [IAP Cloud Console page](https://console.cloud.google.com/security/iap). 131 | - If you don‘t already have an active project, you'll be prompted to 132 | select the project you want to secure with Cloud IAP. Select the project 133 | where your GAE IAP resource is configured. The same project that was used 134 | to configure GCIP must be used for enabling IAP. 135 | - On the **Identity-Aware Proxy** page, under **HTTP Resource**, find the 136 | GAE app you want to restrict access to. The **Published** column shows the 137 | URL of the app. To turn on Cloud IAP for the app, click the **Switch** 138 | icon in the IAP column. 139 | - Toggle the switch on. This will display the IAM side panel. 140 | - To enable external identities, select the "Use external identities for 141 | authorization". Doing so will remove all existing access from the resource. 142 | The previous settings would be restored if you revert to IAM. 143 | - If you have not already enabled GCIP, you will be prompted to do so. 144 | - Identity Platform can now be configured for the selected resource. The 145 | following information needs to be provided in the Identity Platform panel 146 | - Authentication URL: This is the URL noted above. IAP will redirect to this 147 | address for unauthenticated requests to the current resource. This page needs 148 | to be configured to handle authentication. Follow the instructions above to 149 | configure that page. 150 | - Facebook should already be configured for this GCIP project. Select the 151 | project level Facebook IdP in the permissions section. If the IdP was 152 | configured on a GCIP tenant, the corresponding tenant should be selected 153 | instead from the list of tenants. 154 | - Click **SAVE** and you are now ready to use external identities with IAP. 155 | 156 | ## Test access 157 | 158 | - Go to `https://[YOUR_PROJECT_ID].appspot.com`. You should be redirected to 159 | Facebook to sign in. 160 | - After sign-in, you should be redirected back to the original app and your 161 | user's profile populated and displayed. 162 | - Click **Sign Out** link. You should be redirected back to Facebook to sign in. 163 | -------------------------------------------------------------------------------- /sample/authui-firebaseui/database.rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | ".read": false, 4 | ".write": false 5 | } 6 | } -------------------------------------------------------------------------------- /sample/authui-firebaseui/firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "database": { 3 | "rules": "database.rules.json" 4 | }, 5 | "hosting": { 6 | "public": "public", 7 | "ignore": [ 8 | "firebase.json", 9 | "**/.*", 10 | "**/node_modules/**" 11 | ], 12 | "rewrites": [ 13 | { 14 | "source": "**", 15 | "destination": "/index.html" 16 | } 17 | ], 18 | "headers": [ 19 | { 20 | "source": "/**", 21 | "headers": [ 22 | { 23 | "key": "Cache-Control", 24 | "value": "no-cache, no-store, must-revalidate" 25 | } 26 | ] 27 | } 28 | ] 29 | } 30 | } -------------------------------------------------------------------------------- /sample/authui-firebaseui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "iap-external-identities-authentication-quickstart", 3 | "version": "0.0.0", 4 | "description": "Quickstart for IAP external identities Authentication UI with FirebaseUI", 5 | "author": "Google", 6 | "license": "Apache-2.0", 7 | "scripts": { 8 | "preinstall": "cd ../.. && npm install", 9 | "bundle": "webpack --config webpack.config.js", 10 | "prestart": "npm run bundle", 11 | "start": "firebase serve", 12 | "deploy": "npm run prestart && firebase deploy" 13 | }, 14 | "devDependencies": { 15 | "@babel/cli": "^7.10.5", 16 | "@babel/core": "^7.10.5", 17 | "@babel/plugin-proposal-class-properties": "^7.10.4", 18 | "@babel/preset-env": "^7.10.4", 19 | "@babel/register": "^7.10.5", 20 | "babel-loader": "^8.1.0", 21 | "bootstrap": "4.5.0", 22 | "css-loader": "3.6.0", 23 | "file-loader": "6.0.0", 24 | "firebase": "^9.8.3", 25 | "firebase-tools": "^9.21.0", 26 | "firebaseui": "^6.0.0", 27 | "gcip-iap": "^1.0.1", 28 | "handlebars": "^4.7.6", 29 | "jquery": "^3.5.1", 30 | "popper.js": "^1.16.1", 31 | "promise-polyfill": "^8.1.3", 32 | "semistandard": "^14.2.2", 33 | "style-loader": "^1.2.1", 34 | "ts-loader": "^8.2.0", 35 | "url-loader": "^4.1.0", 36 | "url-polyfill": "^1.1.10", 37 | "webpack": "^4.43.0", 38 | "webpack-cli": "^3.3.12", 39 | "whatwg-fetch": "^3.2.0" 40 | }, 41 | "engines": { 42 | "node": ">=10" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /sample/authui-firebaseui/public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Page Not Found 7 | 8 | 23 | 24 | 25 |
26 |

404

27 |

Page Not Found

28 |

The specified file was not found on this website. Please check the URL for mistakes and try again.

29 |
30 | 31 | 32 | -------------------------------------------------------------------------------- /sample/authui-firebaseui/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | IAP External IDs Quickstart 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | -------------------------------------------------------------------------------- /sample/authui-firebaseui/public/style.css: -------------------------------------------------------------------------------- 1 | /** Override FirebaseUI styles. */ 2 | @keyframes fadeIn { 3 | 0% { 4 | opacity: 0; 5 | } 6 | 90% { 7 | opacity: 0; 8 | } 9 | 100% { 10 | opacity: 1; 11 | } 12 | } 13 | 14 | .firebaseui-id-page-spinner { 15 | animation: 3s fadeIn; 16 | } 17 | /** End of overrides. */ 18 | -------------------------------------------------------------------------------- /sample/authui-firebaseui/src/script.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the 10 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | * express or implied. See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | import '../node_modules/bootstrap/dist/css/bootstrap.min.css'; 16 | import '../node_modules/firebaseui/dist/firebaseui.css'; 17 | import '../public/style.css'; 18 | 19 | // Import Firebase dependencies. 20 | import {FacebookAuthProvider} from 'firebase/auth'; 21 | // Import FirebaseUI dependencies. 22 | import * as firebaseui from 'firebaseui'; 23 | // Import GCIP/IAP module. 24 | import * as ciap from 'gcip-iap'; 25 | 26 | /** @return Whether the current browser is Safari. */ 27 | function isSafari(): boolean { 28 | const userAgent = navigator.userAgent.toLowerCase(); 29 | return userAgent.indexOf('safari/') !== -1 && 30 | userAgent.indexOf('chrome/') === -1 && 31 | userAgent.indexOf('crios/') === -1 && 32 | userAgent.indexOf('android/') === -1; 33 | } 34 | 35 | // The list of UI configs for each supported tenant. 36 | const tenantsConfig = { 37 | // Project level IdPs flow. 38 | '*': { 39 | displayName: 'My Organization', 40 | signInOptions: [ 41 | FacebookAuthProvider.PROVIDER_ID, 42 | ], 43 | // Do not trigger immediate redirect in Safari without some user 44 | // interaction. 45 | immediateFederatedRedirect: !isSafari(), 46 | }, 47 | }; 48 | 49 | // Fetch configuration via reserved Firebase Hosting URL. 50 | fetch('/__/firebase/init.json').then((response) => { 51 | return response.json(); 52 | }).then((config) => { 53 | const configs = {}; 54 | configs[config.apiKey] = { 55 | authDomain: config.authDomain, 56 | displayMode: 'optionsFirst', 57 | tenants: tenantsConfig, 58 | }; 59 | // This will handle the underlying handshake for sign-in, sign-out, 60 | // token refresh, safe redirect to callback URL, etc. 61 | const handler = new firebaseui.auth.FirebaseUiHandler( 62 | '#firebaseui-container', configs); 63 | const ciapInstance = new ciap.Authentication(handler); 64 | ciapInstance.start(); 65 | }); 66 | -------------------------------------------------------------------------------- /sample/authui-firebaseui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "noImplicitAny": false, 5 | "sourceMap": true, 6 | "skipLibCheck": true, 7 | "lib": ["dom", "es2015", "es2015.promise"], 8 | "outDir": "lib", 9 | "rootDir": "." 10 | }, 11 | "files": [ 12 | "src/script.ts" 13 | ], 14 | "include": [ 15 | "src/**/*" 16 | ], 17 | "exclude": [ 18 | "node_modules" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /sample/authui-firebaseui/webpack.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2019 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for t`he specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 'use strict'; 17 | 18 | const path = require('path'); 19 | 20 | const config = { 21 | context: __dirname, 22 | entry: { 23 | 'script': './src/script.ts', 24 | }, 25 | output: { 26 | filename: '[name].js', 27 | path: path.resolve(__dirname, './public'), 28 | }, 29 | resolve: { 30 | extensions: ['.js', '.ts'], 31 | alias: { 32 | 'promise-polyfill': path.resolve(__dirname, './node_modules/promise-polyfill/'), 33 | 'url-polyfill': path.resolve(__dirname, './node_modules/url-polyfill/'), 34 | 'whatwg-fetch': path.resolve(__dirname, './node_modules/whatwg-fetch/'), 35 | }, 36 | }, 37 | stats: { 38 | colors: true, 39 | reasons: true, 40 | chunks: true, 41 | }, 42 | module: { 43 | rules: [ 44 | { 45 | test: /\.js$/, 46 | loader: 'babel-loader', 47 | exclude: /node_modules/ 48 | }, 49 | { 50 | test: /\.ts$/, 51 | loader: 'ts-loader', 52 | exclude: /node_modules/ 53 | }, 54 | { 55 | test: /\.css$/, 56 | use: ['style-loader', 'css-loader'] 57 | }, 58 | { 59 | test: /\.(png|woff|woff2|eot|ttf|svg)$/, 60 | loader: 'url-loader?limit=100000' 61 | } 62 | ] 63 | }, 64 | mode: 'none', 65 | optimization: { 66 | minimize: true 67 | } 68 | } 69 | 70 | module.exports = config; 71 | -------------------------------------------------------------------------------- /sample/authui-react/.env: -------------------------------------------------------------------------------- 1 | SKIP_PREFLIGHT_CHECK=true -------------------------------------------------------------------------------- /sample/authui-react/database.rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | ".read": false, 4 | ".write": false 5 | } 6 | } -------------------------------------------------------------------------------- /sample/authui-react/firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "database": { 3 | "rules": "database.rules.json" 4 | }, 5 | "hosting": { 6 | "public": "build", 7 | "ignore": [ 8 | "firebase.json", 9 | "**/.*", 10 | "**/node_modules/**" 11 | ], 12 | "rewrites": [ 13 | { 14 | "source": "**", 15 | "destination": "/index.html" 16 | } 17 | ], 18 | "headers": [ 19 | { 20 | "source": "/**", 21 | "headers": [ 22 | { 23 | "key": "Cache-Control", 24 | "value": "no-cache, no-store, must-revalidate" 25 | } 26 | ] 27 | } 28 | ] 29 | } 30 | } -------------------------------------------------------------------------------- /sample/authui-react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "iap-external-identities-authentication-ui-react", 3 | "version": "0.0.0", 4 | "description": "React-based Authentication UI for IAP external identities", 5 | "author": "Google", 6 | "license": "Apache-2.0", 7 | "dependencies": { 8 | "@types/bootstrap": "^4.5.0", 9 | "@types/jest": "26.0.5", 10 | "@types/node": "14.0.24", 11 | "@types/react": "16.9.43", 12 | "@types/react-dom": "16.9.8", 13 | "@types/react-router": "^5.1.8", 14 | "@types/react-router-dom": "^5.1.5", 15 | "bootstrap": "^4.5.0", 16 | "firebase": "^9.8.3", 17 | "firebaseui": "^6.0.0", 18 | "gcip-iap": "^1.0.1", 19 | "jquery": "^3.5.1", 20 | "react": "^16.13.1", 21 | "react-dom": "^16.13.1", 22 | "react-router": "^5.2.0", 23 | "react-router-dom": "^5.2.0", 24 | "react-scripts": "5.0.1", 25 | "typescript": "3.9.7", 26 | "url-polyfill": "^1.1.10", 27 | "webpack": "4.42.0", 28 | "webpack-dev-server": "^3.11.0", 29 | "whatwg-fetch": "^3.2.0" 30 | }, 31 | "scripts": { 32 | "preinstall": "cd ../.. && npm install", 33 | "react-start": "react-scripts start", 34 | "start": "react-scripts build && firebase serve", 35 | "deploy": "react-scripts build && firebase deploy", 36 | "build": "react-scripts build" 37 | }, 38 | "eslintConfig": { 39 | "extends": "react-app" 40 | }, 41 | "browserslist": { 42 | "production": [ 43 | ">0.2%", 44 | "not dead", 45 | "not op_mini all" 46 | ], 47 | "development": [ 48 | "last 1 chrome version", 49 | "last 1 firefox version", 50 | "last 1 safari version" 51 | ] 52 | }, 53 | "engines": { 54 | "node": ">=10" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /sample/authui-react/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 15 | 16 | 25 | Authentication UI for IAP external identities 26 | 27 | 28 | 29 |
30 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /sample/authui-react/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Auth UI for IAP external ids", 3 | "name": "Authentication UI for IAP external identities", 4 | "start_url": ".", 5 | "display": "standalone", 6 | "theme_color": "#000000", 7 | "background_color": "#ffffff" 8 | } 9 | -------------------------------------------------------------------------------- /sample/authui-react/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /sample/authui-react/src/components/alert.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the 10 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | * express or implied. See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | import React from 'react'; 15 | 16 | export interface AlertParameters { 17 | message?: string; 18 | code?: string; 19 | retry?: () => void; 20 | } 21 | 22 | export class Alert extends React.Component { 23 | render(): JSX.Element { 24 | return ( 25 | 26 | {this.props.message && 27 |
28 | Error {this.props.code} - {this.props.message} 29 | {!!this.props.retry && 30 | Try again 31 | } 32 |
33 | } 34 |
35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /sample/authui-react/src/components/navbar.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the 10 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | * express or implied. See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | import React from 'react'; 15 | 16 | export interface NavbarParameters { 17 | link?: string; 18 | originalUrl?: string; 19 | } 20 | 21 | export class Navbar extends React.Component { 22 | render(): JSX.Element { 23 | return ( 24 | 25 | 39 | 62 | 63 | ); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /sample/authui-react/src/components/privacypolicy.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the 10 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | * express or implied. See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | import React from 'react'; 15 | 16 | const PrivacyPolicy: React.FC = () => { 17 | return ( 18 | 19 |
Term of Service
20 |
Privacy Policy
21 |
22 | ); 23 | } 24 | 25 | export default PrivacyPolicy; 26 | -------------------------------------------------------------------------------- /sample/authui-react/src/components/progressbar.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the 10 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | * express or implied. See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | import React from 'react'; 15 | 16 | const ProgressBar: React.FC = () => { 17 | return ( 18 |
19 |
20 | Loading... 21 |
22 |
23 | ); 24 | } 25 | 26 | export default ProgressBar; 27 | -------------------------------------------------------------------------------- /sample/authui-react/src/components/root.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the 10 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | * express or implied. See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | import React from 'react'; 15 | import { BrowserRouter as Router, Route } from 'react-router-dom'; 16 | import App from './app'; 17 | import FirebaseUi from './firebaseui'; 18 | import PrivacyPolicy from './privacypolicy'; 19 | 20 | const AppRouter: React.FC = () => { 21 | return ( 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 |
30 |
31 | ); 32 | } 33 | 34 | export default AppRouter; 35 | -------------------------------------------------------------------------------- /sample/authui-react/src/components/selecttenant.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the 10 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | * express or implied. See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | import React from 'react'; 15 | 16 | export interface SelectTenantParameters { 17 | tenants: Array<{ 18 | tenantId: string; 19 | tenantDisplayName: string; 20 | }>; 21 | onSelectTenant: (tenantId: string) => void; 22 | } 23 | 24 | export class SelectTenant extends React.Component { 25 | render(): JSX.Element { 26 | return ( 27 |
28 |
29 | Authentication UI for IAP external identities 30 |
31 |
32 |
Select Company
33 |
{e.preventDefault();}}> 34 |
35 | {this.props.tenants.map((tenant, index) => { 36 | return ( 37 | 45 | ); 46 | })} 47 |
48 |
49 |
50 |
51 | ); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /sample/authui-react/src/components/signin.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the 10 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | * express or implied. See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | import React from 'react'; 15 | 16 | export interface SignInParameters { 17 | saml: boolean; 18 | onSignInWithSaml: () => boolean; 19 | onSignInWithGoogle: () => boolean; 20 | onSignInWithFacebook: () => boolean; 21 | onSignInWithEmail: (email: string) => boolean; 22 | } 23 | 24 | export class SignIn extends React.Component { 25 | constructor(props: SignInParameters) { 26 | super(props); 27 | this.state = { 28 | email: '', 29 | }; 30 | } 31 | 32 | handleChange = (event: any) => { 33 | this.setState({ 34 | email: event.target.value, 35 | }); 36 | }; 37 | 38 | render(): JSX.Element { 39 | return ( 40 |
41 |
42 | Authentication UI for IAP external identities 43 |
44 |
45 |
Sign in
46 |
{ 48 | this.props.onSignInWithEmail(this.state.email); 49 | e.preventDefault(); 50 | }}> 51 |
52 | 53 | 60 |
61 | 66 |
67 |

OR

68 | {this.props.saml ? ( 69 | 75 | ) : ( 76 | 77 | 83 | 89 | 90 | )} 91 |
92 |
93 |
94 |
95 | ); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /sample/authui-react/src/components/signinwithemail.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the 10 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | * express or implied. See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | import React from 'react'; 15 | 16 | export interface SignInWithEmailParameters { 17 | email: string; 18 | onSignInWithEmailAndPassword: (password: string) => boolean; 19 | } 20 | 21 | export class SignInWithEmail extends React.Component { 22 | constructor(props: SignInWithEmailParameters) { 23 | super(props); 24 | this.state = { 25 | password: '', 26 | }; 27 | } 28 | 29 | handleChange = (event: any) => { 30 | this.setState({ 31 | password: event.target.value, 32 | }); 33 | }; 34 | 35 | render(): JSX.Element { 36 | return ( 37 |
38 |
39 | Authentication UI for IAP external identities 40 |
41 |
42 |
Sign in
43 |
{ 45 | this.props.onSignInWithEmailAndPassword(this.state.password); 46 | e.preventDefault(); 47 | }}> 48 |
49 |

Enter password for {this.props.email}

50 | 51 | 58 |
59 | 62 |
63 |
64 |
65 | ); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /sample/authui-react/src/components/signout.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the 10 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | * express or implied. See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | import React from 'react'; 15 | 16 | const SignOut: React.FC = () => { 17 | return ( 18 |
19 | Success! You are now signed out. 20 |
21 | ); 22 | } 23 | 24 | export default SignOut; 25 | -------------------------------------------------------------------------------- /sample/authui-react/src/components/signupwithemail.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the 10 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | * express or implied. See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | import React from 'react'; 15 | 16 | export interface SignUpWithEmailParameters { 17 | email: string; 18 | onSignUpWithEmailAndPassword: (displayName: string, password: string) => boolean; 19 | } 20 | 21 | interface SignUpWithEmailState { 22 | password: string; 23 | displayName?: string; 24 | } 25 | 26 | export class SignUpWithEmail extends React.Component { 27 | constructor(props: SignUpWithEmailParameters) { 28 | super(props); 29 | this.state = { 30 | password: '', 31 | }; 32 | } 33 | 34 | handlePasswordChange = (event: any) => { 35 | this.setState({ 36 | displayName: this.state.displayName, 37 | password: event.target.value, 38 | }); 39 | }; 40 | 41 | handleDisplayNameChange = (event: any) => { 42 | this.setState({ 43 | displayName: event.target.value, 44 | password: this.state.password, 45 | }); 46 | }; 47 | 48 | handleSignUpWithEmailAndPassword = () => { 49 | this.props.onSignUpWithEmailAndPassword( 50 | this.state.displayName || '', this.state.password); 51 | }; 52 | 53 | render(): JSX.Element { 54 | return ( 55 |
56 |
57 | Authentication UI for IAP external identities 58 |
59 |
60 |
Sign up
61 |
{ 63 | this.handleSignUpWithEmailAndPassword(); 64 | e.preventDefault(); 65 | }}> 66 |
67 |

Create account for {this.props.email}

68 | 69 | 76 |
77 |
78 | 79 | 86 |
87 | 90 |
91 |
92 |
93 | ); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /sample/authui-react/src/index.css: -------------------------------------------------------------------------------- 1 | .main-container { 2 | max-width: 600px; 3 | margin: 0 auto; 4 | } 5 | 6 | .padded-div { 7 | padding: 10px; 8 | } 9 | 10 | .heading-center { 11 | text-align: center; 12 | justify-content: center; 13 | } 14 | 15 | .hidden { 16 | display: none; 17 | } 18 | 19 | /** https://stackoverflow.com/questions/23584120/line-before-and-after-title-over-image */ 20 | h3.line-through { 21 | color: #777777; 22 | font-size: 1.0rem; 23 | font-weight: 400; 24 | margin: .7em auto; 25 | overflow: hidden; 26 | text-align: center; 27 | width: 100%; 28 | } 29 | 30 | h3.line-through:before, h3.line-through:after { 31 | border-bottom: 1px solid rgba(0,0,0,.125); 32 | content: ""; 33 | display: inline-block; 34 | margin: 0 .5em 0 -55%; 35 | vertical-align: middle; 36 | width: 50%; 37 | } 38 | 39 | h3.line-through:after { 40 | margin: 0 -55% 0 .5em; 41 | } 42 | -------------------------------------------------------------------------------- /sample/authui-react/src/index.tsx: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright 2019 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import React from 'react'; 18 | import ReactDOM from 'react-dom'; 19 | import './index.css'; 20 | import AppRouter from './components/root'; 21 | import * as serviceWorker from './serviceWorker'; 22 | 23 | ReactDOM.render(, document.getElementById('root')); 24 | 25 | // If you want your app to work offline and load faster, you can change 26 | // unregister() to register() below. Note this comes with some pitfalls. 27 | // Learn more about service workers: https://bit.ly/CRA-PWA 28 | serviceWorker.unregister(); 29 | -------------------------------------------------------------------------------- /sample/authui-react/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright 2019 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /// 18 | -------------------------------------------------------------------------------- /sample/authui-react/src/serviceWorker.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright 2019 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | // This optional code is used to register a service worker. 18 | // register() is not called by default. 19 | 20 | // This lets the app load faster on subsequent visits in production, and gives 21 | // it offline capabilities. However, it also means that developers (and users) 22 | // will only see deployed updates on subsequent visits to a page, after all the 23 | // existing tabs open on the page have been closed, since previously cached 24 | // resources are updated in the background. 25 | 26 | // To learn more about the benefits of this model and instructions on how to 27 | // opt-in, read https://bit.ly/CRA-PWA 28 | 29 | const isLocalhost = Boolean( 30 | window.location.hostname === 'localhost' || 31 | // [::1] is the IPv6 localhost address. 32 | window.location.hostname === '[::1]' || 33 | // 127.0.0.1/8 is considered localhost for IPv4. 34 | window.location.hostname.match( 35 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 36 | ) 37 | ); 38 | 39 | type Config = { 40 | onSuccess?: (registration: ServiceWorkerRegistration) => void; 41 | onUpdate?: (registration: ServiceWorkerRegistration) => void; 42 | }; 43 | 44 | export function register(config?: Config) { 45 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 46 | // The URL constructor is available in all browsers that support SW. 47 | const publicUrl = new URL( 48 | (process as { env: { [key: string]: string } }).env.PUBLIC_URL, 49 | window.location.href 50 | ); 51 | if (publicUrl.origin !== window.location.origin) { 52 | // Our service worker won't work if PUBLIC_URL is on a different origin 53 | // from what our page is served on. This might happen if a CDN is used to 54 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 55 | return; 56 | } 57 | 58 | window.addEventListener('load', () => { 59 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 60 | 61 | if (isLocalhost) { 62 | // This is running on localhost. Let's check if a service worker still exists or not. 63 | checkValidServiceWorker(swUrl, config); 64 | 65 | // Add some additional logging to localhost, pointing developers to the 66 | // service worker/PWA documentation. 67 | navigator.serviceWorker.ready.then(() => { 68 | console.log( 69 | 'This web app is being served cache-first by a service ' + 70 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 71 | ); 72 | }); 73 | } else { 74 | // Is not localhost. Just register service worker 75 | registerValidSW(swUrl, config); 76 | } 77 | }); 78 | } 79 | } 80 | 81 | function registerValidSW(swUrl: string, config?: Config) { 82 | navigator.serviceWorker 83 | .register(swUrl) 84 | .then(registration => { 85 | registration.onupdatefound = () => { 86 | const installingWorker = registration.installing; 87 | if (installingWorker == null) { 88 | return; 89 | } 90 | installingWorker.onstatechange = () => { 91 | if (installingWorker.state === 'installed') { 92 | if (navigator.serviceWorker.controller) { 93 | // At this point, the updated precached content has been fetched, 94 | // but the previous service worker will still serve the older 95 | // content until all client tabs are closed. 96 | console.log( 97 | 'New content is available and will be used when all ' + 98 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 99 | ); 100 | 101 | // Execute callback 102 | if (config && config.onUpdate) { 103 | config.onUpdate(registration); 104 | } 105 | } else { 106 | // At this point, everything has been precached. 107 | // It's the perfect time to display a 108 | // "Content is cached for offline use." message. 109 | console.log('Content is cached for offline use.'); 110 | 111 | // Execute callback 112 | if (config && config.onSuccess) { 113 | config.onSuccess(registration); 114 | } 115 | } 116 | } 117 | }; 118 | }; 119 | }) 120 | .catch(error => { 121 | console.error('Error during service worker registration:', error); 122 | }); 123 | } 124 | 125 | function checkValidServiceWorker(swUrl: string, config?: Config) { 126 | // Check if the service worker can be found. If it can't reload the page. 127 | fetch(swUrl) 128 | .then(response => { 129 | // Ensure service worker exists, and that we really are getting a JS file. 130 | const contentType = response.headers.get('content-type'); 131 | if ( 132 | response.status === 404 || 133 | (contentType != null && contentType.indexOf('javascript') === -1) 134 | ) { 135 | // No service worker found. Probably a different app. Reload the page. 136 | navigator.serviceWorker.ready.then(registration => { 137 | registration.unregister().then(() => { 138 | window.location.reload(); 139 | }); 140 | }); 141 | } else { 142 | // Service worker found. Proceed as normal. 143 | registerValidSW(swUrl, config); 144 | } 145 | }) 146 | .catch(() => { 147 | console.log( 148 | 'No internet connection found. App is running in offline mode.' 149 | ); 150 | }); 151 | } 152 | 153 | export function unregister() { 154 | if ('serviceWorker' in navigator) { 155 | navigator.serviceWorker.ready.then(registration => { 156 | registration.unregister(); 157 | }); 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /sample/authui-react/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "noEmit": true, 20 | "jsx": "react", 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /sample/authui/angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "my-app": { 7 | "root": "", 8 | "sourceRoot": "src", 9 | "projectType": "application", 10 | "prefix": "app", 11 | "schematics": {}, 12 | "architect": { 13 | "build": { 14 | "builder": "@angular-devkit/build-angular:browser", 15 | "options": { 16 | "aot": true, 17 | "outputPath": "dist/my-app", 18 | "index": "src/index.html", 19 | "main": "src/main.ts", 20 | "polyfills": "src/polyfills.ts", 21 | "tsConfig": "tsconfig.app.json", 22 | "assets": [ 23 | "src/favicon.ico", 24 | "src/assets" 25 | ], 26 | "styles": [ 27 | "src/styles.css", 28 | "node_modules/bootstrap/dist/css/bootstrap.min.css", 29 | "node_modules/firebaseui/dist/firebaseui.css" 30 | ], 31 | "scripts": [ 32 | "node_modules/jquery/dist/jquery.min.js", 33 | "node_modules/bootstrap/dist/js/bootstrap.min.js" 34 | ] 35 | }, 36 | "configurations": { 37 | "production": { 38 | "fileReplacements": [ 39 | { 40 | "replace": "src/environments/environment.ts", 41 | "with": "src/environments/environment.prod.ts" 42 | } 43 | ], 44 | "optimization": true, 45 | "outputHashing": "all", 46 | "sourceMap": false, 47 | "extractCss": true, 48 | "namedChunks": false, 49 | "aot": true, 50 | "extractLicenses": true, 51 | "vendorChunk": false, 52 | "buildOptimizer": true, 53 | "budgets": [ 54 | { 55 | "type": "initial", 56 | "maximumWarning": "2mb", 57 | "maximumError": "5mb" 58 | }, 59 | { 60 | "type": "anyComponentStyle", 61 | "maximumWarning": "6kb" 62 | } 63 | ] 64 | } 65 | } 66 | }, 67 | "serve": { 68 | "builder": "@angular-devkit/build-angular:dev-server", 69 | "options": { 70 | "browserTarget": "my-app:build" 71 | }, 72 | "configurations": { 73 | "production": { 74 | "browserTarget": "my-app:build:production" 75 | } 76 | } 77 | }, 78 | "extract-i18n": { 79 | "builder": "@angular-devkit/build-angular:extract-i18n", 80 | "options": { 81 | "browserTarget": "my-app:build" 82 | } 83 | }, 84 | "lint": { 85 | "builder": "@angular-devkit/build-angular:tslint", 86 | "options": { 87 | "tsConfig": [ 88 | "tsconfig.app.json", 89 | "tsconfig.spec.json" 90 | ], 91 | "exclude": [ 92 | "**/node_modules/**" 93 | ] 94 | } 95 | } 96 | } 97 | } 98 | }, 99 | "defaultProject": "my-app" 100 | } -------------------------------------------------------------------------------- /sample/authui/database.rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | ".read": false, 4 | ".write": false 5 | } 6 | } -------------------------------------------------------------------------------- /sample/authui/firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "database": { 3 | "rules": "database.rules.json" 4 | }, 5 | "hosting": { 6 | "public": "dist/my-app", 7 | "ignore": [ 8 | "firebase.json", 9 | "**/.*", 10 | "**/node_modules/**" 11 | ], 12 | "rewrites": [ 13 | { 14 | "source": "**", 15 | "destination": "/index.html" 16 | } 17 | ], 18 | "headers": [ 19 | { 20 | "source": "/**", 21 | "headers": [ 22 | { 23 | "key": "Cache-Control", 24 | "value": "no-cache, no-store, must-revalidate" 25 | } 26 | ] 27 | } 28 | ] 29 | } 30 | } -------------------------------------------------------------------------------- /sample/authui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "iap-external-identities-authentication-ui-angular", 3 | "version": "0.0.0", 4 | "description": "Angular-based Authentication UI for IAP external identities", 5 | "author": "Google", 6 | "license": "Apache-2.0", 7 | "dependencies": { 8 | "@angular/animations": "^10.0.4", 9 | "@angular/common": "^10.0.4", 10 | "@angular/compiler": "^10.0.4", 11 | "@angular/core": "^10.0.4", 12 | "@angular/forms": "^10.0.4", 13 | "@angular/platform-browser": "^10.0.4", 14 | "@angular/platform-browser-dynamic": "^10.0.4", 15 | "@angular/router": "^10.0.4", 16 | "core-js": "^2.6.9", 17 | "rxjs": "^6.5.2", 18 | "tslib": "^2.0.0", 19 | "zone.js": "~0.10.3" 20 | }, 21 | "scripts": { 22 | "preinstall": "cd ../.. && npm install", 23 | "ng": "ng", 24 | "ng-serve": "ng serve", 25 | "start": "ng build && firebase serve", 26 | "build": "ng build", 27 | "lint": "ng lint", 28 | "deploy": "ng build && firebase deploy" 29 | }, 30 | "devDependencies": { 31 | "@angular-devkit/build-angular": "^0.1002.0", 32 | "@angular-devkit/build-ng-packagr": "~0.1000.3", 33 | "@angular/cli": "^10.0.3", 34 | "@angular/compiler-cli": "^10.0.4", 35 | "@angular/language-service": "^10.0.4", 36 | "@types/bootstrap": "4.5.0", 37 | "@types/jasmine": "^3.5.11", 38 | "@types/jasminewd2": "^2.0.8", 39 | "@types/jquery": "^3.5.0", 40 | "@types/node": "^14.0.24", 41 | "bootstrap": "4.5.0", 42 | "codelyzer": "^6.0.0", 43 | "firebase": "^9.8.3", 44 | "firebaseui": "^6.0.0", 45 | "gcip-iap": "^1.0.1", 46 | "jasmine-core": "^3.5.0", 47 | "jasmine-spec-reporter": "~5.0.0", 48 | "jquery": "^3.5.1", 49 | "karma": "^5.1.0", 50 | "karma-chrome-launcher": "^3.1.0", 51 | "karma-coverage-istanbul-reporter": "^3.0.3", 52 | "karma-jasmine": "^3.3.1", 53 | "karma-jasmine-html-reporter": "^1.5.4", 54 | "ng-packagr": "^10.0.0", 55 | "promise-polyfill": "^8.1.3", 56 | "protractor": "^7.0.0", 57 | "ts-node": "^8.10.2", 58 | "tslint": "~6.1.0", 59 | "typescript": "~3.9.7", 60 | "url-polyfill": "^1.1.10", 61 | "whatwg-fetch": "^3.2.0" 62 | }, 63 | "engines": { 64 | "node": ">=10" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /sample/authui/src/app/alert.component.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the 10 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | * express or implied. See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | import { Component, Input } from '@angular/core'; 16 | 17 | @Component({ 18 | selector: 'alert', 19 | template: ` 20 | 21 | 27 | 28 | `, 29 | }) 30 | export class AlertComponent { 31 | @Input() public code?: string; 32 | @Input() public message?: string; 33 | @Input() public retry?: () => any; 34 | 35 | /** Triggers the retry if the error is recoverable. */ 36 | public runRetry() { 37 | if (this.retry) { 38 | (this.retry as any)(); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /sample/authui/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the 10 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | * express or implied. See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | import {NgModule} from '@angular/core'; 16 | import {RouterModule, Routes} from '@angular/router'; 17 | import {BrowserModule} from '@angular/platform-browser'; 18 | import {SelectTenantComponent} from './selecttenant.component'; 19 | import {SignInComponent} from './signin.component'; 20 | import {SignInWithEmailComponent} from './signinwithemail.component'; 21 | import {SignUpWithEmailComponent} from './signupwithemail.component'; 22 | import {SignOutComponent} from './signout.component'; 23 | import {AlertComponent} from './alert.component'; 24 | import {ProgressBarComponent} from './progressbar.component'; 25 | import {NavBarComponent} from './navbar.component'; 26 | import {PageNotFoundComponent} from './pagenotfound.component'; 27 | import {FirebaseUiComponent} from './firebaseui.component'; 28 | import {RootComponent} from './root.component'; 29 | import {PrivacyPolicyComponent} from './privacypolicy.component'; 30 | import {AppComponent} from './app.component'; 31 | 32 | const appRoutes: Routes = [ 33 | { path: '', component: FirebaseUiComponent }, 34 | { path: 'custom', component: AppComponent }, 35 | { path: 'tos', component: PrivacyPolicyComponent }, 36 | { path: 'privacypolicy', component: PrivacyPolicyComponent }, 37 | { path: '**', component: PageNotFoundComponent }, 38 | ]; 39 | 40 | @NgModule({ 41 | imports: [ 42 | BrowserModule, 43 | RouterModule.forRoot( 44 | appRoutes, 45 | { 46 | enableTracing: false, 47 | }, 48 | ), 49 | ], 50 | declarations: [ 51 | AppComponent, NavBarComponent, SelectTenantComponent, SignInComponent, 52 | SignInWithEmailComponent, SignUpWithEmailComponent, SignOutComponent, 53 | AlertComponent, ProgressBarComponent, PageNotFoundComponent, 54 | FirebaseUiComponent, RootComponent, PrivacyPolicyComponent, 55 | ], 56 | entryComponents: [ 57 | AppComponent, NavBarComponent, SelectTenantComponent, SignInComponent, 58 | SignInWithEmailComponent, SignUpWithEmailComponent, SignOutComponent, 59 | AlertComponent, ProgressBarComponent, PageNotFoundComponent, 60 | FirebaseUiComponent, RootComponent, PrivacyPolicyComponent, 61 | ], 62 | bootstrap: [ 63 | RootComponent, 64 | ], 65 | }) 66 | export class AppModule {} 67 | -------------------------------------------------------------------------------- /sample/authui/src/app/firebaseui.component.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the 10 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | * express or implied. See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | import { Component } from '@angular/core'; 16 | // Import Firebase dependencies. 17 | // tslint:disable-next-line:no-submodule-imports 18 | import { EmailAuthProvider, GoogleAuthProvider, FacebookAuthProvider } from 'firebase/auth'; 19 | // Import FirebaseUI dependencies. 20 | import * as firebaseui from 'firebaseui'; 21 | // Import GCIP/IAP module. 22 | import * as ciap from 'gcip-iap'; 23 | 24 | // The list of UI configs for each supported tenant. 25 | const tenantsConfig = { 26 | // Project-level IdPs flow. 27 | _: { 28 | displayName: 'My Organization', 29 | signInOptions: [ 30 | EmailAuthProvider.PROVIDER_ID, 31 | GoogleAuthProvider.PROVIDER_ID, 32 | FacebookAuthProvider.PROVIDER_ID, 33 | ], 34 | tosUrl: '/tos', 35 | privacyPolicyUrl: '/privacypolicy', 36 | }, 37 | 38 | /* 39 | Single tenant flow. 40 | Maps to tenants in a sample project. Find the tenants from your project and populate it here. 41 | */ 42 | 'tenant-a-l41js': { 43 | displayName: 'My Company', 44 | signInOptions: [ 45 | EmailAuthProvider.PROVIDER_ID, 46 | { 47 | provider: 'saml.okta-cicp-app', 48 | providerName: 'SAML', 49 | buttonColor: '#4666FF', 50 | iconUrl: 'https://www.gstatic.com/firebasejs/ui/2.0.0/images/auth/anonymous.png', 51 | }, 52 | GoogleAuthProvider.PROVIDER_ID, 53 | FacebookAuthProvider.PROVIDER_ID, 54 | ], 55 | signInFlow: 'redirect', 56 | // A boolean which determines whether to immediately redirect to the provider's site or 57 | // instead show the default 'Sign in with Provider' button when there is only a single 58 | // federated provider in signInOptions. In order for this option to take effect, the 59 | // signInOptions must only hold a single federated provider (like 'google.com') and 60 | // signInFlow must be set to 'redirect'. 61 | immediateFederatedRedirect: false, 62 | tosUrl: '/tos', 63 | privacyPolicyUrl: '/privacypolicy', 64 | }, 65 | /* 66 | Multiple tenants flow. 67 | Maps to tenants in a sample project. Find the tenants from your project and populate it here. 68 | */ 69 | 'tenant-a-omli0': { 70 | displayName: 'Company A', 71 | buttonColor: '#007bff', 72 | iconUrl: 'https://www.gstatic.com/firebasejs/ui/2.0.0/images/auth/anonymous.png', 73 | signInOptions: [ 74 | EmailAuthProvider.PROVIDER_ID, 75 | GoogleAuthProvider.PROVIDER_ID, 76 | { 77 | provider: 'saml.okta-cicp-app', 78 | providerName: 'SAML', 79 | buttonColor: '#4666FF', 80 | iconUrl: 'https://www.gstatic.com/firebasejs/ui/2.0.0/images/auth/anonymous.png', 81 | }, 82 | ], 83 | tosUrl: '/tos', 84 | privacyPolicyUrl: '/privacypolicy', 85 | }, 86 | 'tenant-b-9t9qm': { 87 | displayName: 'Company B', 88 | buttonColor: '#007bff', 89 | iconUrl: 'https://www.gstatic.com/firebasejs/ui/2.0.0/images/auth/anonymous.png', 90 | signInOptions: [ 91 | EmailAuthProvider.PROVIDER_ID, 92 | { 93 | provider: 'saml.okta-cicp-app', 94 | providerName: 'SAML', 95 | buttonColor: '#4666FF', 96 | iconUrl: 'https://www.gstatic.com/firebasejs/ui/2.0.0/images/auth/anonymous.png', 97 | }, 98 | ], 99 | tosUrl: '/tos', 100 | privacyPolicyUrl: '/privacypolicy', 101 | }, 102 | 'tenant-c-hhtch': { 103 | displayName: 'Company C', 104 | buttonColor: '#007bff', 105 | iconUrl: 'https://www.gstatic.com/firebasejs/ui/2.0.0/images/auth/anonymous.png', 106 | signInOptions: [ 107 | EmailAuthProvider.PROVIDER_ID, 108 | ], 109 | tosUrl: '/tos', 110 | privacyPolicyUrl: '/privacypolicy', 111 | }, 112 | 'tenant-d-9x6ia': { 113 | displayName: 'Company D', 114 | buttonColor: '#007bff', 115 | iconUrl: 'https://www.gstatic.com/firebasejs/ui/2.0.0/images/auth/anonymous.png', 116 | signInOptions: [ 117 | EmailAuthProvider.PROVIDER_ID, 118 | ], 119 | tosUrl: '/tos', 120 | privacyPolicyUrl: '/privacypolicy', 121 | }, 122 | }; 123 | 124 | @Component({ 125 | selector: 'firebaseui', 126 | template: ` 127 |
128 | 129 |
130 | {{title}} 131 |
132 |
133 |
134 |
135 | `, 136 | }) 137 | 138 | export class FirebaseUiComponent { 139 | public title?: string; 140 | constructor() { 141 | // Fetch configuration via reserved Firebase Hosting URL. 142 | fetch('/__/firebase/init.json').then((response) => { 143 | return response.json(); 144 | }).then((config) => { 145 | const configs = {}; 146 | configs[config.apiKey] = { 147 | authDomain: config.authDomain, 148 | callbacks: { 149 | // The callback to trigger when the tenant selection page 150 | // is shown. 151 | selectTenantUiShown: () => { 152 | this.title = 'Select Employer'; 153 | }, 154 | // The callback to trigger when the tenant selection page 155 | // is hidden. 156 | selectTenantUiHidden: () => { 157 | this.title = null; 158 | }, 159 | // The callback to trigger when the sign-in page 160 | // is shown. 161 | signInUiShown: (tenantId) => { 162 | const configKey = tenantId ? tenantId : '_'; 163 | this.title = tenantsConfig[configKey].displayName; 164 | }, 165 | beforeSignInSuccess: (user) => { 166 | // Do additional processing on user before sign-in is 167 | // complete. 168 | return Promise.resolve(user); 169 | }, 170 | }, 171 | displayMode: 'optionsFirst', 172 | // The terms of service URL and privacy policy URL for the page 173 | // where the user selects a tenant or enters an email for tenant/provider 174 | // matching. 175 | tosUrl: '/tos', 176 | privacyPolicyUrl: '/privacypolicy', 177 | tenants: tenantsConfig, 178 | }; 179 | // This will handle the underlying handshake for sign-in, sign-out, 180 | // token refresh, safe redirect to callback URL, etc. 181 | const handler = new firebaseui.auth.FirebaseUiHandler( 182 | '#firebaseui-container', configs); 183 | const ciapInstance = new ciap.Authentication(handler); 184 | ciapInstance.start(); 185 | }); 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /sample/authui/src/app/navbar.component.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the 10 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | * express or implied. See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | import { Component, Input } from '@angular/core'; 16 | 17 | @Component({ 18 | selector: 'navbar', 19 | template: ` 20 | 32 | 33 | 56 | `, 57 | }) 58 | export class NavBarComponent { 59 | @Input() public originalUrl: string; 60 | @Input() public link: string; 61 | 62 | constructor() { 63 | // On get original URL button click, call getOriginalURL() and populate the 64 | // result in the opened modal. 65 | } 66 | 67 | /** Displays the dialog with the original URL. */ 68 | public showModal() { 69 | $('#originalUrlModal').modal('show'); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /sample/authui/src/app/pagenotfound.component.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the 10 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | * express or implied. See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | import { Component } from '@angular/core'; 16 | 17 | @Component({ 18 | selector: 'page-not-found', 19 | template: ` 20 | 87 |
88 |

404

89 |

Page Not Found

90 |

91 | The specified file was not found on this website. 92 | Please check the URL for mistakes and try again. 93 |

94 |
95 | `, 96 | }) 97 | export class PageNotFoundComponent {} 98 | -------------------------------------------------------------------------------- /sample/authui/src/app/privacypolicy.component.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the 10 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | * express or implied. See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | import { Component } from '@angular/core'; 16 | 17 | @Component({ 18 | selector: 'privacy-policy', 19 | template: ` 20 |
Term of Service
21 |
Privacy Policy
22 | `, 23 | }) 24 | export class PrivacyPolicyComponent {} 25 | -------------------------------------------------------------------------------- /sample/authui/src/app/progressbar.component.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the 10 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | * express or implied. See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | import { Component } from '@angular/core'; 16 | 17 | @Component({ 18 | selector: 'progress-bar', 19 | template: ` 20 |
21 |
22 | Loading... 23 |
24 |
25 | `, 26 | }) 27 | export class ProgressBarComponent { 28 | } 29 | -------------------------------------------------------------------------------- /sample/authui/src/app/root.component.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the 10 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | * express or implied. See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | import { Component } from '@angular/core'; 16 | 17 | @Component({ 18 | selector: 'root', 19 | template: ` 20 | 21 | `, 22 | }) 23 | export class RootComponent {} 24 | -------------------------------------------------------------------------------- /sample/authui/src/app/selecttenant.component.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the 10 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | * express or implied. See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | import { Component, Input } from '@angular/core'; 16 | 17 | @Component({ 18 | selector: 'select-tenant', 19 | template: ` 20 |
21 | 24 |
25 |
Select Company
26 |
27 |
28 | 37 |
38 |
39 |
40 |
41 |
42 | `, 43 | }) 44 | export class SelectTenantComponent { 45 | @Input() public tenants: Array<{tenantId: string, tenantDisplayName: string}>; 46 | @Input() public onclick: (tenantId: string) => void; 47 | } 48 | -------------------------------------------------------------------------------- /sample/authui/src/app/signin.component.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the 10 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | * express or implied. See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | import { Component, Input } from '@angular/core'; 16 | 17 | @Component({ 18 | selector: 'sign-in', 19 | template: ` 20 |
21 | 24 |
25 |
Sign in
26 |
27 |
28 | 29 | 34 |
35 | 40 |
41 |

OR

42 | 43 | 49 | 50 | 51 | 57 | 63 | 64 |
65 |
66 |
67 |
68 |
69 | `, 70 | }) 71 | export class SignInComponent { 72 | @Input() public saml: boolean; 73 | @Input() public onSignInWithSaml: () => boolean; 74 | @Input() public onSignInWithGoogle: () => boolean; 75 | @Input() public onSignInWithFacebook: () => boolean; 76 | @Input() public onSignInWithEmail: (email: string) => boolean; 77 | } 78 | -------------------------------------------------------------------------------- /sample/authui/src/app/signinwithemail.component.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the 10 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | * express or implied. See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | import { Component, Input } from '@angular/core'; 16 | 17 | @Component({ 18 | selector: 'sign-in-with-email', 19 | template: ` 20 |
21 | 24 |
25 |
Sign in
26 |
27 |
28 |

Enter password for {{email}}

29 | 30 | 35 |
36 | 40 |
41 |
42 |
43 | `, 44 | }) 45 | export class SignInWithEmailComponent { 46 | @Input() public email: string; 47 | @Input() public onSignInWithEmailAndPassword: (password: string) => boolean; 48 | } 49 | -------------------------------------------------------------------------------- /sample/authui/src/app/signout.component.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the 10 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | * express or implied. See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | import { Component } from '@angular/core'; 16 | 17 | @Component({ 18 | selector: 'sign-out', 19 | template: ` 20 | 23 | `, 24 | }) 25 | export class SignOutComponent { 26 | } 27 | -------------------------------------------------------------------------------- /sample/authui/src/app/signupwithemail.component.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the 10 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | * express or implied. See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | import { Component, Input } from '@angular/core'; 16 | 17 | @Component({ 18 | selector: 'sign-up-with-email', 19 | template: ` 20 |
21 | 24 |
25 |
Sign up
26 |
27 |
28 |

Create account for {{email}}

29 | 30 | 35 |
36 |
37 | 38 | 43 |
44 | 48 |
49 |
50 |
51 | `, 52 | }) 53 | export class SignUpWithEmailComponent { 54 | @Input() public email: string; 55 | @Input() public onSignUpWithEmailAndPassword: (displayName: string, password: string) => boolean; 56 | } 57 | -------------------------------------------------------------------------------- /sample/authui/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the 10 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | * express or implied. See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | export const environment = { 16 | production: true, 17 | }; 18 | -------------------------------------------------------------------------------- /sample/authui/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the 10 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | * express or implied. See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | // This file can be replaced during build by using the `fileReplacements` array. 16 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 17 | // The list of file replacements can be found in `angular.json`. 18 | 19 | export const environment = { 20 | production: false, 21 | }; 22 | -------------------------------------------------------------------------------- /sample/authui/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Authentication UI for IAP external identities 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /sample/authui/src/karma.conf.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the 10 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | * express or implied. See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | // Karma configuration file, see link for more information 16 | // https://karma-runner.github.io/1.0/config/configuration-file.html 17 | 18 | module.exports = function (config) { 19 | config.set({ 20 | basePath: '', 21 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 22 | plugins: [ 23 | require('karma-jasmine'), 24 | require('karma-chrome-launcher'), 25 | require('karma-jasmine-html-reporter'), 26 | require('karma-coverage-istanbul-reporter'), 27 | require('@angular-devkit/build-angular/plugins/karma') 28 | ], 29 | client: { 30 | clearContext: false 31 | }, 32 | coverageIstanbulReporter: { 33 | dir: require('path').join(__dirname, '../coverage'), 34 | reports: ['html', 'lcovonly'], 35 | fixWebpackSourcePaths: true 36 | }, 37 | reporters: ['progress', 'kjhtml'], 38 | port: 9876, 39 | colors: true, 40 | logLevel: config.LOG_INFO, 41 | autoWatch: true, 42 | browsers: ['Chrome'], 43 | singleRun: false 44 | }); 45 | }; -------------------------------------------------------------------------------- /sample/authui/src/main.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the 10 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | * express or implied. See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | import './polyfills'; 16 | 17 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 18 | 19 | import { AppModule } from './app/app.module'; 20 | 21 | platformBrowserDynamic().bootstrapModule(AppModule).then((ref) => { 22 | if ((window as any).ngRef) { 23 | (window as any).ngRef.destroy(); 24 | } 25 | (window as any).ngRef = ref; 26 | // tslint:disable-next-line 27 | }).catch((err) => console.error(err)); 28 | -------------------------------------------------------------------------------- /sample/authui/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright 2019 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * This file includes polyfills needed by Angular and is loaded before the app. 19 | * You can add your own extra polyfills to this file. 20 | * 21 | * This file is divided into 2 sections: 22 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 23 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 24 | * file. 25 | * 26 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 27 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 28 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 29 | * 30 | * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html 31 | */ 32 | 33 | /*************************************************************************************************** 34 | * BROWSER POLYFILLS 35 | */ 36 | 37 | /** IE9, IE10 and IE11 requires all of the following polyfills. */ 38 | // import 'core-js/es6/symbol'; 39 | // import 'core-js/es6/object'; 40 | // import 'core-js/es6/function'; 41 | // import 'core-js/es6/parse-int'; 42 | // import 'core-js/es6/parse-float'; 43 | // import 'core-js/es6/number'; 44 | // import 'core-js/es6/math'; 45 | // import 'core-js/es6/string'; 46 | // import 'core-js/es6/date'; 47 | // import 'core-js/es6/array'; 48 | // import 'core-js/es6/regexp'; 49 | // import 'core-js/es6/map'; 50 | // import 'core-js/es6/set'; 51 | 52 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 53 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 54 | 55 | /** IE10 and IE11 requires the following to support `@angular/animation`. */ 56 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 57 | 58 | 59 | /** Evergreen browsers require these. */ 60 | /* tslint:disable:no-submodule-imports */ 61 | import 'core-js/es6/reflect'; 62 | import 'core-js/es7/reflect'; 63 | /* tslint:enable:no-submodule-imports */ 64 | 65 | 66 | /** ALL Firefox browsers require the following to support `@angular/animation`. */ 67 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 68 | 69 | 70 | 71 | /*************************************************************************************************** 72 | * Zone JS is required by Angular itself. 73 | */ 74 | /* tslint:disable:no-submodule-imports */ 75 | import 'zone.js/dist/zone'; // Included with Angular CLI. 76 | /* tslint:enable:no-submodule-imports */ 77 | 78 | /*************************************************************************************************** 79 | * APPLICATION IMPORTS 80 | */ 81 | 82 | /** 83 | * Date, currency, decimal and percent pipes. 84 | * Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10 85 | */ 86 | // import 'intl'; // Run `npm install --save intl`. 87 | -------------------------------------------------------------------------------- /sample/authui/src/styles.css: -------------------------------------------------------------------------------- 1 | .main-container { 2 | max-width: 600px; 3 | margin: 0 auto; 4 | } 5 | 6 | .padded-div { 7 | padding: 10px; 8 | } 9 | 10 | .heading-center { 11 | text-align: center; 12 | justify-content: center; 13 | } 14 | 15 | .hidden { 16 | display: none; 17 | } 18 | 19 | /** https://stackoverflow.com/questions/23584120/line-before-and-after-title-over-image */ 20 | h3.line-through { 21 | color: #777777; 22 | font-size: 1.0rem; 23 | font-weight: 400; 24 | margin: .7em auto; 25 | overflow: hidden; 26 | text-align: center; 27 | width: 100%; 28 | } 29 | 30 | h3.line-through:before, h3.line-through:after { 31 | border-bottom: 1px solid rgba(0,0,0,.125); 32 | content: ""; 33 | display: inline-block; 34 | margin: 0 .5em 0 -55%; 35 | vertical-align: middle; 36 | width: 50%; 37 | } 38 | 39 | h3.line-through:after { 40 | margin: 0 -55% 0 .5em; 41 | } 42 | -------------------------------------------------------------------------------- /sample/authui/src/test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the 10 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | * express or implied. See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | /* tslint:disable:no-submodule-imports */ 16 | import 'zone.js/dist/zone-testing'; 17 | import { getTestBed } from '@angular/core/testing'; 18 | import { 19 | BrowserDynamicTestingModule, 20 | platformBrowserDynamicTesting, 21 | } from '@angular/platform-browser-dynamic/testing'; 22 | /* tslint:enable:no-submodule-imports */ 23 | 24 | declare const require: any; 25 | 26 | getTestBed().initTestEnvironment( 27 | BrowserDynamicTestingModule, 28 | platformBrowserDynamicTesting(), 29 | ); 30 | 31 | const context = require.context('./', true, /\.spec\.ts$/); 32 | context.keys().map(context); 33 | -------------------------------------------------------------------------------- /sample/authui/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/app", 5 | "types": [] 6 | }, 7 | "files": [ 8 | "src/main.ts", 9 | "src/polyfills.ts" 10 | ], 11 | "include": [ 12 | "src/**/*.d.ts" 13 | ] 14 | } -------------------------------------------------------------------------------- /sample/authui/tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "./dist/out-tsc", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "module": "es2020", 9 | "moduleResolution": "node", 10 | "emitDecoratorMetadata": true, 11 | "experimentalDecorators": true, 12 | "skipLibCheck": true, 13 | "target": "es5", 14 | "typeRoots": [ 15 | "node_modules/@types" 16 | ], 17 | "lib": [ 18 | "es2018", 19 | "dom" 20 | ] 21 | } 22 | } -------------------------------------------------------------------------------- /sample/authui/tsconfig.json: -------------------------------------------------------------------------------- 1 | /* 2 | This is a "Solution Style" tsconfig.json file, and is used by editors and TypeScript’s language server to improve development experience. 3 | It is not intended to be used to perform a compilation. 4 | 5 | To learn more about this file see: https://angular.io/config/solution-tsconfig. 6 | */ 7 | { 8 | "files": [], 9 | "references": [ 10 | { 11 | "path": "./tsconfig.app.json" 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /sample/authui/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/spec", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts", 12 | "src/polyfills.ts" 13 | ], 14 | "include": [ 15 | "src/**/*.spec.ts", 16 | "src/**/*.d.ts" 17 | ] 18 | } --------------------------------------------------------------------------------