@monash/portal-auth 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- package/README.md +39 -0
- package/dist/components/AuthContext.js +152 -0
- package/dist/utils/fbProxy.js +63 -0
- package/package.json +26 -0
package/README.md
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
# @monash/portal-auth
|
2
|
+
|
3
|
+
Monash react components
|
4
|
+
|
5
|
+
## Features
|
6
|
+
|
7
|
+
- ES6 syntax, managed with Prettier + Eslint and Stylelint
|
8
|
+
- Unit testing via Jest
|
9
|
+
- React 17
|
10
|
+
|
11
|
+
## Install
|
12
|
+
|
13
|
+
```sh
|
14
|
+
yarn add @monash/portal-auth
|
15
|
+
// or
|
16
|
+
npm i @monash/portal-auth
|
17
|
+
```
|
18
|
+
|
19
|
+
### Requirements
|
20
|
+
|
21
|
+
- Node.js `v14.x` or later
|
22
|
+
- React 17
|
23
|
+
- Firebase
|
24
|
+
|
25
|
+
|
26
|
+
### Usage
|
27
|
+
|
28
|
+
```js
|
29
|
+
import { AuthContext } from '@monash/portal-auth';
|
30
|
+
import { App } from './app';
|
31
|
+
import * as ReactDOM from 'react-dom';
|
32
|
+
|
33
|
+
ReactDOM.render(
|
34
|
+
<AuthContext>
|
35
|
+
<App />
|
36
|
+
</AuthContext>,
|
37
|
+
document.getElementById('root')
|
38
|
+
);
|
39
|
+
```
|
@@ -0,0 +1,152 @@
|
|
1
|
+
import React, { useState, createContext, useEffect } from 'react';
|
2
|
+
import firebase from 'firebase/app';
|
3
|
+
import 'firebase/auth';
|
4
|
+
|
5
|
+
const AuthContext = createContext();
|
6
|
+
|
7
|
+
const getSNSProvider = sns => {
|
8
|
+
switch (sns) {
|
9
|
+
case 'Google':
|
10
|
+
return new firebase.auth.GoogleAuthProvider();
|
11
|
+
default:
|
12
|
+
throw new Error("[Auth] Unsupported SNS: " + sns);
|
13
|
+
}
|
14
|
+
};
|
15
|
+
|
16
|
+
const SSO_ATTEMPTED = 'sso_attempted';
|
17
|
+
const SSO_ERROR = 'sso_error';
|
18
|
+
const TRUE_VALUES = ['true', 'True', 'TRUE', true, 1];
|
19
|
+
const FIREBASE_CUSTOM_TOKEN = 'firebase_custom_token';
|
20
|
+
|
21
|
+
const ssoSession = {
|
22
|
+
setAttempted: attempted => {
|
23
|
+
sessionStorage.setItem(SSO_ATTEMPTED, TRUE_VALUES.includes(attempted));
|
24
|
+
},
|
25
|
+
isAttempted: () => {
|
26
|
+
return TRUE_VALUES.includes(sessionStorage.getItem(SSO_ATTEMPTED));
|
27
|
+
}
|
28
|
+
};
|
29
|
+
|
30
|
+
const getCallbackParams = () => {
|
31
|
+
if (window.location.search === "") return { code: null, state: null };
|
32
|
+
|
33
|
+
const urlParams = new URLSearchParams(window.location.search);
|
34
|
+
const code = urlParams.get('code');
|
35
|
+
const state = urlParams.get('state');
|
36
|
+
|
37
|
+
const errorCode = urlParams.get('error');
|
38
|
+
const errorDescription = urlParams.get('error_description');
|
39
|
+
if (errorCode) {
|
40
|
+
let error = new Error(errorDescription);
|
41
|
+
error.code = errorCode;
|
42
|
+
throw error;
|
43
|
+
}
|
44
|
+
|
45
|
+
return { code, state };
|
46
|
+
};
|
47
|
+
|
48
|
+
const AuthProvider = props => {
|
49
|
+
|
50
|
+
const auth = props.fbAuth;
|
51
|
+
const oktaLoginUrl = props.oktaLoginUrl;
|
52
|
+
const callbackPath = props.callbackPath;
|
53
|
+
const tokenApi = props.tokenApi;
|
54
|
+
const nolanding = props.nolanding;
|
55
|
+
const loadingPage = props.loadingPage;
|
56
|
+
const targetLocation = props.targetLocation;
|
57
|
+
|
58
|
+
const [error, setError] = useState(null);
|
59
|
+
const [user, setUser] = useState(null);
|
60
|
+
const [redirectUrl, setRedirectUrl] = useState(null);
|
61
|
+
|
62
|
+
const [attempting, setAttempting] = useState(false);
|
63
|
+
useEffect(() => {
|
64
|
+
const handleCallback = async () => {
|
65
|
+
if (!attempting) {
|
66
|
+
setAttempting(true);
|
67
|
+
try {
|
68
|
+
if (!user) {
|
69
|
+
const { code, state } = getCallbackParams();
|
70
|
+
const resp = await fetch(`${tokenApi}?code=${code}&state=${state}`, { mode: 'cors' });
|
71
|
+
const authResult = await resp.json();
|
72
|
+
await auth.setPersistence(firebase.auth.Auth.Persistence.SESSION);
|
73
|
+
const credential = await auth.signInWithCustomToken(authResult[FIREBASE_CUSTOM_TOKEN]);
|
74
|
+
setUser(credential.user);
|
75
|
+
setRedirectUrl(authResult['redirect_url']);
|
76
|
+
}
|
77
|
+
} catch (err) {
|
78
|
+
setError({ type: SSO_ERROR, error: err });
|
79
|
+
setAttempting(false);
|
80
|
+
if (nolanding) ssoSession.setAttempted(false);
|
81
|
+
}
|
82
|
+
}
|
83
|
+
};
|
84
|
+
|
85
|
+
if (window.location.pathname === callbackPath) {
|
86
|
+
handleCallback();
|
87
|
+
} else if (nolanding) {
|
88
|
+
if (ssoSession.isAttempted()) {
|
89
|
+
auth.onAuthStateChanged(user => {
|
90
|
+
if (user) {
|
91
|
+
setUser(user);
|
92
|
+
}
|
93
|
+
});
|
94
|
+
} else {
|
95
|
+
loginOkta(targetLocation);
|
96
|
+
}
|
97
|
+
} else {
|
98
|
+
auth.onAuthStateChanged(user => {
|
99
|
+
if (user) {
|
100
|
+
setUser(user);
|
101
|
+
}
|
102
|
+
});
|
103
|
+
}
|
104
|
+
});
|
105
|
+
|
106
|
+
const [redirecting, setRedirecting] = useState(false);
|
107
|
+
useEffect(() => {
|
108
|
+
if (redirectUrl && !redirecting) {
|
109
|
+
window.location = redirectUrl;
|
110
|
+
setRedirecting(true);
|
111
|
+
}
|
112
|
+
}, [redirectUrl, redirecting]);
|
113
|
+
|
114
|
+
const loginOkta = targetLocation => {
|
115
|
+
setRedirectUrl(`${oktaLoginUrl}?redirect_uri=${encodeURIComponent(targetLocation || window.location)}`);
|
116
|
+
if (nolanding) ssoSession.setAttempted(true);
|
117
|
+
};
|
118
|
+
|
119
|
+
const loginSNS = sns => {
|
120
|
+
const provider = getSNSProvider(sns);
|
121
|
+
if (nolanding) ssoSession.setAttempted(true);
|
122
|
+
return auth.signInWithRedirect(provider);
|
123
|
+
};
|
124
|
+
|
125
|
+
const loginSMS = (phoneNo, recaptchaContainerId) => {
|
126
|
+
// 'recaptcha-container' is the ID of an element in the DOM.
|
127
|
+
const applicationVerifier = new firebase.auth.RecaptchaVerifier(recaptchaContainerId ? recaptchaContainerId : 'recaptcha-container');
|
128
|
+
const provider = new firebase.auth.PhoneAuthProvider();
|
129
|
+
provider.verifyPhoneNumber(phoneNo, applicationVerifier).then(function (verificationId) {
|
130
|
+
var verificationCode = window.prompt('Please enter the verification code that was sent to your mobile device.');
|
131
|
+
return firebase.auth.PhoneAuthProvider.credential(verificationId, verificationCode);
|
132
|
+
}).then(function (phoneCredential) {
|
133
|
+
return firebase.auth().signInWithCredential(phoneCredential);
|
134
|
+
});
|
135
|
+
if (nolanding) ssoSession.setAttempted(true);
|
136
|
+
};
|
137
|
+
|
138
|
+
const logout = target => auth.signOut().then(() => {
|
139
|
+
setUser(null);
|
140
|
+
setError(null);
|
141
|
+
setRedirectUrl(target || "/");
|
142
|
+
if (nolanding) ssoSession.setAttempted(false);
|
143
|
+
});
|
144
|
+
|
145
|
+
return React.createElement(
|
146
|
+
AuthContext.Provider,
|
147
|
+
{ value: { user, error, loginOkta, loginSNS, loginSMS, logout } },
|
148
|
+
!redirectUrl && !attempting && props.children || attempting && loadingPage
|
149
|
+
);
|
150
|
+
};
|
151
|
+
|
152
|
+
export { AuthContext, AuthProvider };
|
@@ -0,0 +1,63 @@
|
|
1
|
+
const localhost = 'localhost';
|
2
|
+
const functions_region = 'australia-southeast1';
|
3
|
+
|
4
|
+
const getProxiedFirestore = (fb, fs, host, ssl) => {
|
5
|
+
fs = fs || fb.firestore();
|
6
|
+
if (ssl === undefined) ssl = !host.includes(localhost);
|
7
|
+
|
8
|
+
if (!ssl) {
|
9
|
+
let [hostname, port] = host.split(':');
|
10
|
+
if (!port) {
|
11
|
+
port = 5001;
|
12
|
+
} else {
|
13
|
+
port = Number.parseInt(port);
|
14
|
+
}
|
15
|
+
fs.useEmulator(hostname, port);
|
16
|
+
fs.settings({ experimentalForceLongPolling: true });
|
17
|
+
} else {
|
18
|
+
fs.settings({ host, ssl, experimentalForceLongPolling: true });
|
19
|
+
}
|
20
|
+
return fs;
|
21
|
+
};
|
22
|
+
|
23
|
+
const getProxiedFunctions = (fb, fn, host, ssl) => {
|
24
|
+
fn = fn || fb.functions(functions_region);
|
25
|
+
if (ssl === undefined) ssl = !host.includes(localhost);
|
26
|
+
|
27
|
+
if (!ssl) {
|
28
|
+
let [hostname, port] = host.split(':');
|
29
|
+
if (!port) {
|
30
|
+
port = 5001;
|
31
|
+
} else {
|
32
|
+
port = Number.parseInt(port);
|
33
|
+
}
|
34
|
+
fn.useEmulator(hostname, port);
|
35
|
+
} else {
|
36
|
+
fn.useFunctionsEmulator(`https://${host}`);
|
37
|
+
}
|
38
|
+
return fn;
|
39
|
+
};
|
40
|
+
|
41
|
+
const getProxiedAuth = (fb, auth, host, ssl) => {
|
42
|
+
auth = auth || fb.auth();
|
43
|
+
if (ssl === undefined) ssl = !host.includes(localhost);
|
44
|
+
|
45
|
+
auth.useEmulator(`${ssl ? 'https' : 'http'}://${host}`);
|
46
|
+
const removeElements = elms => elms.forEach(el => el.remove());
|
47
|
+
// console.debug("[DEBUG] class firebase-emulator-warning: ", document.querySelectorAll(".firebase-emulator-warning"))
|
48
|
+
removeElements(document.querySelectorAll(".firebase-emulator-warning"));
|
49
|
+
|
50
|
+
return auth;
|
51
|
+
};
|
52
|
+
|
53
|
+
const proxy = fb => {
|
54
|
+
return {
|
55
|
+
firestore: (host, ssl, fs) => getProxiedFirestore(fb, fs, host, ssl),
|
56
|
+
functions: (host, ssl, fn) => getProxiedFunctions(fb, fn, host, ssl),
|
57
|
+
auth: (host, ssl, auth) => getProxiedAuth(fb, auth, host, ssl)
|
58
|
+
// database: () => fb.database(), // No more proxy for RTDB
|
59
|
+
// storage: () => fb.storage(), // TODO
|
60
|
+
};
|
61
|
+
};
|
62
|
+
|
63
|
+
export { proxy, functions_region };
|
package/package.json
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
{
|
2
|
+
"name": "@monash/portal-auth",
|
3
|
+
"version": "0.0.4",
|
4
|
+
"private": false,
|
5
|
+
"description": "Monash react components",
|
6
|
+
"license": "MIT",
|
7
|
+
"author": "hmonsh",
|
8
|
+
"main": "dist/components/AuthContext.js",
|
9
|
+
"scripts": {
|
10
|
+
"build": "node scripts/script.js",
|
11
|
+
"test": "exit 0"
|
12
|
+
},
|
13
|
+
"publishConfig": {
|
14
|
+
"access": "public"
|
15
|
+
},
|
16
|
+
"dependencies": {
|
17
|
+
"firebase": "^9.8.4",
|
18
|
+
"react": "17.0.2",
|
19
|
+
"react-dom": "17.0.2"
|
20
|
+
},
|
21
|
+
"devDependencies": {
|
22
|
+
"@babel/runtime": "^7.18.3",
|
23
|
+
"@babel/cli": "^7.18.3",
|
24
|
+
"@babel/core": "^7.18.3"
|
25
|
+
}
|
26
|
+
}
|