Setup
I have a typical web app setup:
A backend service running at https://mybackendsvc.com
Users are authenticated with mybackendsvc.com, and have a session cookie.
Svelte/Sapper running at https://myfrontend.com
I'm using Sapper preload to fetch some data from https://mybackendsvc.com. Here's routes/mydata/[id].svelte:
<script context="module">
export async function preload(page, session) {
const res = await this.fetch(
`https://mybackendsvc.com/api/public/${page.params.id}`,
{
credentials: 'include' // sends the session cookie to mybackendsvc.com
}
);
if (res.status == 200) {
let myData = await res.json();
return { myData };
} else if (res.status === 401) {
this.error(401, "Unauthorized");
} else {
this.error(500, "Something went wrong");
}
}
</script>
<script>
export let myData;
</script>
<div>
{myData}
</div>
Note the use of fetch with credentials included:
{
credentials: 'include'
}
The problem: Where "preload" executes matters
If a user navigates to mydata/[id] directly, preload runs on the node server, Sapper doesn't include the user's session cookie (likely because the node server doesn't have access to it), so mybackendsvc.com responds with 401. However if a user navigates to mydata/[id] via a link inside my app, preload is run browser-side, the browser includes the session cookie, and the call succeeds.
How can I make Sapper guarantee that fetch includes credentials in an api call? Or can you not use preload with authenticated API calls, only public API calls?
Related
When trying to add an auth exchange to my urql client, it gets run on the server when the app starts and on the client subsequent times until refresh. The problem is in my getAuth function, which is as follows:
const getAuth = async ({ authState }) => {
const token = localStorage.getItem('5etoken');
if (!authState) {
if (token) {
return { token };
}
return null;
}
if (token) {
const decoded = jwt.decode(token) as jwt.JwtPayload;
if (decoded.exp !== undefined && decoded.exp < Date.now() / 1000) {
return { token };
}
}
return null;
};
When I run my app, I get an error saying localStorage is undefined. If I check that the function is running in the browser, then my token never gets set on app start and I'm logged out when I refresh the page, so I can't use that approach. I've tried multiple approaches:
Using dynamic imports with ssr set to false
Creating the client in a useEffect hook
Using next-urql's withUrqlClient HOC only using the auth exchange when in the browser
None of what I tried worked and I'm running out of ideas.
I eventually figured out that createClient was being called on the server side. I managed to force it to run in the browser by creating the client in a useEffect hook. I'm not sure why creating it in a useEffect didn't work months ago.
According to the documentation, you should use a SECRET_TOKEN to prevent unauthorized access to your revalidate API route i.e.
https://<your-site.com>/api/revalidate?secret=<token>
But how are you supposed to call that route from the frontend and keep the token secret?
For example, if you have a simple POST that you then want to trigger the revalidate off of, you'd have to expose your secret token via NEXT_PUBLIC to be able to use it:
function handleSubmit(payload) {
axios.post(POST_URL, payload)
.then(() => {
axios.get(`/api/revalidate?secret=${process.env.NEXT_PUBLIC_SECRET_TOKEN}`)
})
.then(() => {
// redirect to on-demand revalidated page
})
}
What am I missing here? How can you call the API route through the frontend without exposing the SECRET_TOKEN?
I've been trying out On-Demand ISR and stumbled on a similar problem. I was trying to revalidate data after CRUD actions from my Admin dashboard living on the client, behind protected routes ("/admin/...").
If you have an authentication process setup and you're using Next-Auth's JWT strategy, it gives you access to the getToken() method, which decrypts the JWT of the current authenticated user.
You can then use whatever information you have passed through your callbacks to validate the request instead of relying on a SECRET_TOKEN.
import type { NextApiRequest, NextApiResponse } from "next";
import { getToken } from "next-auth/jwt";
const secret = process.env.NEXTAUTH_SECRET;
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
const user = await getToken({ req, secret });
if (!user || user.role !== "ADMIN") {
return res.status(401).json({ message: "Revalidation not authorized"});
}
try {
// unstable_revalidate is being used in Next 12.1
// I'm passing the revalidation url through the query params
await res.unstable_revalidate(req.query.url as string);
return res.json({ revalidated: true });
} catch (err) {
return res.status(500).send("Error revalidating");
}
}
The Next.js video demo don't actually use a SECRET_KEY.
https://www.youtube.com/watch?v=BGexHR1tuOA
So I guess I'll just have to omit it and hope nobody abuses the revalidate API?
I think you need to create one file called ".env".
Inside the file, you put the params .env like this:
NEXT_PUBLIC_SECRET_TOKEN=123password
You must install the dependency dotenv:
npm i dotenv
and then you can call inside your function like this
function handleSubmit(payload) {
axios.post(POST_URL, payload)
.then(() => {
axios.get(`/api/revalidate?secret=${process.env.NEXT_PUBLIC_SECRET_TOKEN}`)
})
.then(() => {
// redirect to on-demand revalidated page
})
}
I have a basic next.js application that does two things:
provide an authentication mechanism using keycloak
talk to a backend server that authorizes each request using the keycloak-access-token
I use the #react-keycloak/ssr library to achieve this. The problem now is that after I login and get redirected back to my application the cookie that contains the kcToken is empty. After I refresh my page it works like expected.
I understand that maybe my entire process flow is wrong. If so, what is the "usual" way to achieve what is mentioned above?
export async function getServerSideProps(context) {
const base64KcToken = context.req.cookies.kcToken // the cookie that keycloak places after login
const kcToken = base64KcToken ? Buffer.from(base64KcToken, "base64") : ""
// the backend server passes the token along to keycloak for role-based authorization
const res = await fetch(`${BACKEND_URL}/info`, {
headers: {
"Authorization": "Bearer " + kcToken
}
})
const data = await res.json()
// ... exception handling is left out for readability ...
return {
props: {
data
}
}
}
export default function Home({data}) {
const router = useRouter() // the next.js client side router to redirect to keycloak
const { keycloak, initialized } = useKeycloak() // keycloak instance configured in _app.js
if (keycloak && !initialized && keycloak.createLoginUrl) router.push(keycloak.createLoginUrl())
return (
<div> ... some jsx that displays data ... </div>
)
}
This process basically works but it feels really bad because a user that gets redirected after login is not able to see the fetched data unless he refreshes the entire page. This is because when getServerSideProps() is called right after redirect the base64KcToken is not there yet.
Also everything related to the login-status (eg. logout button) only gets displayed after ~1sec, when the cookie is loaded by the react-keycloak library.
I'm trying to redirect a user to a login page if the user is not logged in when he tries to access certain pages with the following code.
// middlware/authenticated.js
import firebase from 'firebase'
export default function ({ store, redirect }) {
let user = firebase.auth().currentUser
store.state.user = user //this doesn't work
if (!user) {
console.log('redirect')
return redirect('/login')
}
}
However, the problem is with this code when I refresh a page I'm redirected to login page although without using the middleware, I can stay in the same page with logged in. For some reasons, which I don't know why, firebase can't work in middleware.
How should I modify this middleware or implement this function?
Thanks.
//middleware/authenticated.js
export default function ({
store,
redirect
}) {
if (!store.getters['index/isAuthenticated']) {
return redirect('/login')
}
}
//post.vue
async mounted () {
if (process.browser) {
let user;
if (!this.user) user = await auth(); // this auth is a plugin
await Promise.all([
this.user ? Promise.resolve() : this.$store.dispatch("setUser", { user: user || null })
]);
this.isLoaded = true;
}
},
//plugins/auth.js
import firebase from '~/plugins/firebase'
function auth () {
return new Promise((resolve, reject) => {
firebase.auth().onAuthStateChanged((user) => {
resolve(user || false)
})
})
}
export default auth
By default Firebase persists the users logged in status on successful authentication. This example uses the session, to store the user uid and cookies to store the users token and used in situations where the sessions has ended (example when browser is closed) and then a new session started but where the user is still authenticated according to Firebase. In cases like these the user will not need to sign in to view protected resources.
Your basic Middleware to protect it should look like this (if you have a Store Module called User)
export default function ({ store, redirect }) {
if (!store.getters['modules/user/isAuthenticated']) {
return redirect('/auth/signin')
}
}
In your main Store you use the ServerInit Function to get the User if there is one saved in the Cookies and load it into your User Store Module which will be used for verification in the Middleware.
Your User Store Module should look like this, and keep in mind that you remove the Cookie when you Log the User out so that he is fully logged out.
I used the things i mentioned above as the beginning of my Authentication and modified it a bit, which you can also do. Most of the credit goes to davidroyer who has set up this nice Github Repo which includes all needed files as a good example on how to accomplish your goal.
i have a meteor app where i'm using nginx with an internal SSO service to authenticate. I'm able to do this successfully and retrieve user details in the nginx set http headers on the server Meteor.onConnection method.
At this point, i'm not sure what the best approach is to get access to the user details on the client side. I feel like i should use the built in Meteor Accounts but i'm not sure how to initiate the login process from the client since the user will not actually be logging in through the Meteor client but through a redirect that happens through nginx. I feel like i need a way to automatically initiate the login process on the meteor side to set up the meteor.users collection appropriately, but i can't figure out a way to do that.
Checkout the answers here. You can pass the userId (or in whatever you want to pass the user) through nginx to the server then onto the client to login. You can generate and insert the token in a Webapp.connectHandler.
import { Inject } from 'meteor/meteorhacks:inject-initial';
// server/main.js
Meteor.startup(() => {
WebApp.connectHandlers.use("/login",function(req, res, next) {
Fiber(function() {
var userId = req.headers["user-id"]
if (userId){
var stampedLoginToken = Accounts._generateStampedLoginToken();
//check if user exists
Accounts._insertLoginToken(userId, stampedLoginToken);
Inject.obj('auth', {
'loginToken':stampedLoginToken
},res);
return next()
}
}).run()
})
}
Now you can login on the client side with the help of the meteor-inject-initial package
import { Inject } from 'meteor/meteorhacks:inject-initial';
// iron router
Router.route('/login', {
action: function() {
if (!Meteor.userId()){
Meteor.loginWithToken(Inject.getObj('auth').loginToken.token,
function(err,res){
if (err){
console.log(err)
}
}
)
} else {
Router.go('/home')
}
},
});