I am building an app (Main App) that authenticates through a seperate app (Auth App). I am able to signInWithCustomToken but the auth state does not persist between client browser refreshes even though onAuthStateChanged runs with the user after signInWithCustomToken.
Below is the authentication flow:
Users open the Main App, and click a button to open a popup that displays the Auth App.
window.open(AUTH_URL, 'window', 'width=400,height=600');
On the Auth App users create a Firebase account with email and
password.
firebase.auth().signInWithEmailAndPassword(email, password)
The Auth App makes a request to the Auth App's server to
generate a custom token.
// client side code
const idToken = await firebase.auth().currentUser.getIdToken()
const token = api.generateTokenAPIRequest(idToken);
// server side code
const generateTokenAPIRequest = (idToken) => {
const { uid } = await admin.auth().verifyIdToken(idToken);
return await admin.auth().createCustomToken(uid);
};
This custom token is passed back to the Main App via a postMessage.
window.opener.postMessage({ token }, APP_URL);
window.close();
When the Main App receives the authentication message, it signs the user in with the custom token.
window.onmessage = (e: MessageEvent) => {
if (e.origin !== AUTH_URL) { return; }
const { idToken } = e.data;
if (!idToken) return;
firebase.auth().signInWithCustomToken(idToken)
};
I listen to firebase.auth().onAuthStateChanged. This function runs correctly with the new user account but does NOT run again when I refresh the page. It is as if the user is not being stored in the Main App's storage.
It appears this was caused by an unrelated bug where firebase.auth().signOut() was being called unexpectedly. However I wanted to call out a few items in case someone stumbles upon this.
Make sure your service account is the same one you are using on your authenticated app. You can generate an Admin service account through the Firebase Console.
The post message approach has problems where a hacker can grab the ID token, sign in with it and then do all the actions the users can do. Make sure you check the origin of the post message like I do above. There are additional measures that can probably be put in place. I believe this is how the typical "Sign in with Google" works.
There are approaches with sessions that have been posted on Medium. Namely https://dev.to/johncarroll/how-to-share-firebase-authentication-across-subdomains-1ka8 and https://dev.to/brianburton/cross-domain-firebase-authentication-a-simple-approach-337k
Supporting this use case is currently an open issue https://github.com/firebase/firebase-js-sdk/issues/2303
Happy coding!
Related
I'm starting to use Firebase Authentication in my Next.js app.
I've enabled Email/Password authentication method and I've created the user directly from the Firebase console. I don't want to allow user sign up yet.
I've seen that the user email is not verified by default, so I started implementing the logic to do so with the provided sendEmailVerification method, just pretty simple code within the event of a button click:
const handleClick = async () => {
if (!user) {
console.warn(`Shouldn't request a verification email if user is not logged in!.`)
return
}
await sendEmailVerification(user)
}
But surprisingly, after that, the user's email gets automatically verified, and no email is even sent.
What am I missing?
I'm currently integrating a frontend with a 3rd party backend that offers sign-in with social media. I'm using the Google JS SDK https://accounts.google.com/gsi/client which works fine with the one-tap login as it returns an ID Token which the backend requires. However, the downside is, if the user doesn't have a Google session, the prompt won't show.
If the user isn't logged in to Google, I've managed to prompt a login form and request a token on successful login, however, it only returns an access token. Is any way to request an ID token?
Example:
google.accounts.id.prompt(async notification => {
if (notification.getNotDisplayedReason() === 'opt_out_or_no_session') {
const tokenClient = google.accounts.oauth2.initTokenClient({
client_id: googleClientId,
scope: 'openid email profile',
})
tokenClient.callback = response => {
// response only has access token and no ID Token
}
tokenClient.requestAccessToken({ prompt: 'consent' })
}
window.addEventListener('load', () => {
google.accounts.id.initialize({
client_id: googleClientId,
callback: (user: CredentialResponse) => {
const { credential: idToken } = user
// I need idToken in the above requestAccessToken too
},
})
})
})
You are initializing two different namespaces in the example you have provided. The first one initializes oauth2 namespace, which starts the authorisation flow. This flow results in the acquisition of an access_token as you've realised.
The second one initializes the id namespace, which is responsible from the authentication flow. This returns an id_token, which is what you need indeed.
To keep using authentication flow beyond the capability of one-tap log in, you can render a Sign-In with Google button using the authentication initialisation. Simple initialise google.account.id.initialize() as you would. Then call one-tap prompt.
google.accounts.id.prompt();
Then in addition to that, you can render the button:
google.accounts.id.renderButton(document.getElementById("g-btn"), {
type: "standard",
logo_alignment: "left"
});
And
<div class="google-button" id="g-btn"></div>
Regardless of how the user decides to sign-in, it'll lead to the same callback method you've defined in the initialize() call.
I am attempting to implement 0Auth user authorization for my Next.js app using MoneyButton API. I am able to trigger the authorization request with client.requestAuthorization('auth.user_identity:read','http://localhost:3000');
And it works smoothly redirecting me to MoneyButton permission consent and back to my app with the code and state params in URL -> ?code=6aa72eef702eb710cd22715d797cf7d27e06532a&state=38984b9d-3af0-48f1-8b5f-3fa47f4dfd9d
There is client.handleAuthorizationResponse(); method for handle the response. That method automatically gets the tokens from the query parameters and set the internal state of the client to use them. Also it saves the credentials in local storage, so the user stays logged in with Money Button if they close the browser.
But unfortunately i don't know how to use this method after being redirected back to my app. I am using it in Authuser function, but requestAuthorization triggers redirect to moneybutton, so rest of the function is not executed. How to use handleAuthorization after being redirected back to application?
https://docs.moneybutton.com/docs/api/auth/api-auth-jsclient.html - here are the MoneyButton docs
I am also considering to add MoneyButton as custom 0Auth provider in NextAuth.js to make integrations faster in the future.
Authuser.js
const { MoneyButtonClient } = require('#moneybutton/api-client')
export default function Authuser () {
const client = new MoneyButtonClient('MYAPP_OAUTH_IDENTIFIER_CODE');
client.requestAuthorization('auth.user_identity:read','http://localhost:3000');
client.handleAuthorizationResponse();
const refreshToken = client.getRefreshToken();
client.setRefreshToken(refreshToken)
}
You need to make sure that client.handleAuthorizationResponse(); is run client side (not server side render) after the moneybutton auth has redirected back:
if ((new URLSearchParams(window.location.search)).has('code')) {
await client.handleAuthorizationResponse()
const accessToken = await client.getValidAccessToken()
...
}
I’m in web dev since many years ago, but I still do not believe you can easily implement user authorization in front-end apps or browser extensions.
While front-end authentication can be achieved (cookies, jwt, etc.) and works fairly well, the same is not for authorization. The common example is when you want to restrict access to some content and/or functionalities to logged-in users only.
I am inspecting many browser extensions and web apps, and I usually find something like this pseudocode:
if (user.isLogged === true) {
// code to show ui components and actions
} else {
// code to show ui components and actions
}
which is highly insecure.
For instance, this is coming from an extension available on the chrome web store:
function initApp() {
firebase
.auth()
.onAuthStateChanged(function (user) {
if (user) {
// User is signed in. var uid = user.uid; window.location.href = "app.html";
const uid = user.uid;
const name = user.displayName;
getUserData(uid);
// Plus a lot of other code to show/hide ui components and actions
} else { }
document
.getElementById('quickstart-button')
.disabled = false;
});
document
.getElementById('quickstart-button')
.addEventListener('click', startSignIn, false);
}
The only secure way is to actually load HTML and JS chunks dynamically only when and if the user is authorized to access that page/functionality, where the server decides which chunks serve to the current user based on his role, and then those chunks are injected at runtime. But this is, on one hand, failing with the spa architecture itself because it's very near to serving the app server-side (it's never the client to decide what to show). It also requires injecting HTML markup dynamically using JS which is not ideal for security. Plus, this is something not trivial to implement, so I guess the majority of js apps out there are handling restricted content/areas using the paradigm shown above, based on the current in-memory/store (or in cookie/storage) state of the current user, which is highly insecure and can be easily manipulated by the end-user. Especially in browser extensions where you are not even allowed to obfuscate the code.
Am I missing something?
TL;DR you are right, don't rely on front-end security. There is none. Grab the secret content from a backend and require authentication.
You mentioned SPAs so let's analyze the situation there.
Assume you have a React App with a main component looking similar to this:
function App() {
const { loggedIn, isSpecialUser } = useContext(authContext);
if (loggedIn)
return <MainPage />;
if (isSpecialUser)
return <SuperSecretPage />;
return <LoginPage />;
}
The loggedIn and isSpecialUser state is dynamically set after the user logged in (or relogin by cookies, jwt, whatever).
Therefore the restricted areas for MainPage and SuperSecretPage will only show after the user logged in.
So what if the user looks at the source of the SuperSecretPage?
He should see something like this:
function App() {
const { token } = useContext(authContext);
const { data } = useApi(getSuperSecretContent, token);
return <div>{data}</div>;
}
Even if the super secret content is static for all user, it should be fetched with a separate call that requires a token. Access rights must be checked by the backend. If you just write your content in there, it can be seen by everyone.
Edit Response to OP's comment. He wrote
a malicious user has at least 2 options: 1) change the js content of the main App component, to let's say return ; when the user is not logged in at all; 2) manually change the ajax callback to set his status to Superuser regardless of the actual response.
For both ways, the user would just break the clients rendering but not the security. In any case, the client must make an API-call to receive the secret content:
const { data } = useApi(getSuperSecretContent, token);
useApi is an abstract hook that uses some sort of API (e.g. REST-API) to fetch the content during runtime (ajax). The second parameter is a token. This token must be some sort of authentication token the servers sends the user after login. This token here is the crucial part (see OAuth, JWT). For each ajax call to get the super secret content, a valid token must be supplied. The backend then has to check if the token is valid. If and only if the token is valid, the backend returns the super secret content.
Pseudocode useApi:
function useApi(apiCall, token) {
make ajax request {
endpoint: apiCall,
authorization header: token
}
if success:
return data
else if auth error:
return error
}
Pseudocode backend:
get request to super secret content:
if token is not valid:
return auth error;
else
return super secret content
The user cannot trick the backend into sending him the super secret content by manipulating the frontend state.
Edit 2 Response to OP's confusion about authentication and authorization.
You must use the token also for authorization. In case of a JWT for instance, the token itself can include infos about rights management, otherwise you could query access rights from a database for a given user.
This must happen during the request handling #backend side.
Pseudocode backend e.g.:
get request to super secret content:
get access rights for token
if access rights include super secret resources:
return super secret content;
else
return auth error;
I am building a react native application and am using Firebase, more specifically firestore, in order to manage my data. My current objective is to implement an auto login feature on my app, where if the user exits the app, I want them to stay signed in, unless they manually hit the Sign Out button before exiting the app. Here is my current process of doing this:
When the user logs into the app, I sign them in by:
firebase.auth().signInWithEmailAndPassword(email, password).
I then get their idToken by:
let authIdToken = "";
firebase
.auth()
.currentUser.getIdToken(true)
.then(function (idToken) {
authIdToken = idToken
})
.catch(function (error) {
console.log(error)
});
I then want to save this token into the phone, so when the user opens the app again, I can fetch this token and check its validity. If it is valid, then I can log the user in using their idToken. In react native, I can do this by doing:
AsyncStorage.setItem(
"userData",
JSON.stringify({
token: token,
})
);
Now when the app loads up:
const startScreen = props => {
useEffect(() => {
const tryLogin = async () => {
const userData = await AsyncStorage.getItem("userData");
const transformedData = JSON.parse(userData);
const { token } = transformedData;
await firebase
.auth()
.verifyIdToken(token, true)
.then((payload) => {
console.log(true)
})
.catch((error) => {
if (error.code == "auth/id-token-revoked") {
// Token has been revoked. Inform the user to reauthenticate or signOut() the user.
console.log("revoked")
} else {
console.log("error")
}
});
};
tryLogin();
}, []);
The Issue: When I try to verify the token this way, I am met with the following error: firebase.auth().verifyIdToken is not a function.
I read through the documentation and am unsure of how else to verify this token using JS. How do I verify it? Let me know if my verification process is incorrect and how it should be done. I am new to using firestore and doing authentication in general and hope to learn how to do it the right way.
Another helpful note: This is how I am configuring my firestore: !firebase.apps.length ? firebase.initializeApp(firebaseConfig) : {};
Thanks!
I then want to save this token into the phone, so when the user opens the app again, I can fetch this token and check its validity.
This is completely unnecessary. Firebase Auth with persist the signed in user, and automatically refresh the token without you having to do anything. All you need to do is listen to when updates to the token are made available, and act on the new token as needed. You can establish an ID token listener using onIdTokenChanged as shown in the linked API documentation:
firebase.auth().onIdTokenChanged(function(user) {
if (user) {
// User is signed in or token was refreshed.
}
});
Once you have this token, you know that the user is successfully signed in. There is nothing left to do. There is no need to use it to sign in.
Also, you can't verify the token on the frontend. The verifyIdToken method you're looking at is for the Admin SDK only, which only runs on the backend. The idea is that you get the token on the fronend, then pass it to the backend as described in the documentation for the Admin SDK. The backend uses this to securely determine if the user on the frontend is who they say they are.
Since you didn't say if you have a backend or not, dealing with this token might not be necessary at all. If you just want to know when the user is signed in (even if they are just returning to the page after being away, then you can skip everything above and just use an auth state observer. Again, Firebase Auth persists information about the user so you don't have to sign them in again. The observer will tell you when the automatic sign-in is complete, or if they are not signed in at all.