I'm trying to figure out an appropriate way of doing authentication, which I know is a touchy subject on the GitHub issue page.
My authentication is simple. I store a JWT token in the session. I send it to a different server for approval. If I get back true, we keep going, if I get back false, it clears the session and puts sends them to the main page.
In my server.js file I have the following (note- I am using the example from nextjs learn and just adding isAuthenticated):
function isAuthenticated(req, res, next) {
//checks go here
//if (req.user.authenticated)
// return next();
// IF A USER ISN'T LOGGED IN, THEN REDIRECT THEM SOMEWHERE
res.redirect('/');
}
server.get('/p/:id', isAuthenticated, (req, res) => {
const actualPage = '/post'
const queryParams = { id: req.params.id }
app.render(req, res, actualPage, queryParams)
})
This works as designed. If I refresh the page /p/123, it will redirect to the /. However, if I go there via a next/link href, it doesn't. Which I believe is because it's not using express at this point but next's custom routing.
Is there a way I can bake in a check for every single next/link that doesn't go through express so that I can make sure the user is logged in?
Tim from the next chat helped me solve this. Solution can be found here but I will quote him so you all can see:
You can do the check in _app.js getInitialProps and redirect like this
Example of how to use it
_app.js documentation
I've also created an example skeleton template you can take a look at.
--
EDIT July 2021 - WARNING: This is an outdated solution and has not been confirmed to work with the latest versions of next.js. Use skeleton template at your own risk.
Edit: Updated answer for Next 12.2+
Note: The below contents is copied from the official blog post since SO generally discourages links that can become stale/dead over time
https://nextjs.org/blog/next-12-2#middleware-stable
Middleware is now stable in 12.2 and has an improved API based on feedback from users.
// middleware.ts
import { NextRequest, NextResponse } from 'next/server';
// If the incoming request has the "beta" cookie
// then we'll rewrite the request to /beta
export function middleware(req: NextRequest) {
const isInBeta = JSON.parse(req.cookies.get('beta') || 'false');
req.nextUrl.pathname = isInBeta ? '/beta' : '/';
return NextResponse.rewrite(req.nextUrl);
}
// Supports both a single value or an array of matches
export const config = {
matcher: '/',
};
Migration guide
https://nextjs.org/docs/messages/middleware-upgrade-guide
Breaking changes
No Nested Middleware
No Response Body
Cookies API Revamped
New User-Agent Helper
No More Page Match Data
Executing Middleware on Internal Next.js Requests
How to upgrade
You should declare one single Middleware file in your application, which should be located next to the pages directory and named without an _ prefix. Your Middleware file can still have either a .ts or .js extension.
Middleware will be invoked for every route in the app, and a custom matcher can be used to define matching filters. The following is an example for a Middleware that triggers for /about/* and /dashboard/:path*, the custom matcher is defined in an exported config object:
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
return NextResponse.rewrite(new URL('/about-2', request.url))
}
// Supports both a single string value or an array of matchers
export const config = {
matcher: ['/about/:path*', '/dashboard/:path*'],
}
Edit: Outdated answer for next > 12 and < 12.2
With the release of Next.js 12, there's now beta support for middleware using Vercel Edge Functions.
https://nextjs.org/blog/next-12#introducing-middleware
Middleware uses a strict runtime that supports standard Web APIs like fetch. > This works out of the box using next start, as well as on Edge platforms like Vercel, which use Edge Functions.
To use Middleware in Next.js, you can create a file pages/_middleware.js. In this example, we use the standard Web API Response (MDN):
// pages/_middleware.js
export function middleware(req, ev) {
return new Response('Hello, world!')
}
JWT Authentication example
https://github.com/vercel/examples/tree/main/edge-functions/jwt-authentication
in next.config.js:
const withTM = require('#vercel/edge-functions-ui/transpile')()
module.exports = withTM()
in pages/_middleware.js:
import { NextRequest, NextResponse } from 'next/server'
import { setUserCookie } from '#lib/auth'
export function middleware(req: NextRequest) {
// Add the user token to the response
return setUserCookie(req, NextResponse.next())
}
in pages/api/_middleware.js:
import type { NextRequest } from 'next/server'
import { nanoid } from 'nanoid'
import { verifyAuth } from '#lib/auth'
import { jsonResponse } from '#lib/utils'
export async function middleware(req: NextRequest) {
const url = req.nextUrl
if (url.searchParams.has('edge')) {
const resOrPayload = await verifyAuth(req)
return resOrPayload instanceof Response
? resOrPayload
: jsonResponse(200, { nanoid: nanoid(), jwtID: resOrPayload.jti })
}
}
in pages/api/index.js:
import type { NextApiRequest, NextApiResponse } from 'next'
import { verify, JwtPayload } from 'jsonwebtoken'
import { nanoid } from 'nanoid'
import { USER_TOKEN, JWT_SECRET_KEY } from '#lib/constants'
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== 'GET') {
return res.status(405).json({
error: { message: 'Method not allowed' },
})
}
try {
const token = req.cookies[USER_TOKEN]
const payload = verify(token, JWT_SECRET_KEY) as JwtPayload
res.status(200).json({ nanoid: nanoid(), jwtID: payload.jti })
} catch (err) {
res.status(401).json({ error: { message: 'Your token has expired.' } })
}
}
There is no middleware for no API routes in NextJS, but there are HOCs, which you can use to connect to db - select the user, etc:
https://hoangvvo.com/blog/nextjs-middleware
Related
I'm using the library next-csrf (https://github.com/j0lv3r4/next-csrf) to protect api routes in my next.js application.
I followed the documentation but the api now returns an error 500:
{"message":"Signed cookie string must be provided."}
Here's the code:
/lib/csrf.js:
import { nextCsrf } from 'next-csrf';
const options = {
secret: `${process.env.CSRF_SECRET}`,
};
export const { csrf, csrfToken } = nextCsrf(options);
Page that calls the api:
import { useState, useEffect } from 'react';
import axios from 'axios';
import { withRouter } from 'next/router';
import { Page, PostBlock } from '#/components';
const Main = ({ router, csrfToken }) => {
const [postsData, setPostsData] = useState({ posts: [], page: 0, pages: 0 });
function fetchData() {
axios
.get('/api/articles', {
headers: { 'XSRF-TOKEN': csrfToken },
params: {
page: router.query?.page,
lang: router.locale,
tag: router.query.tag,
},
})
.then(response => {
setPostsData(response.data);
})
.catch(error => console.log(error));
}
useEffect(() => {
fetchData();
}, []);
return (
<Page title='Home' className='home-template'>
<div id='grid' className='post-grid'>
{postsData.posts?.map(post => {=
return (
<PostBlock
featured={post.featured}
key={post.slug}
/>
);
})}
</div>
</Page>
);
};
export default withRouter(Main);
The token works and I can see the header in the network tab:
Api route:
import { getPosts } from '../../../utils/index';
import { csrf } from '../../../lib/csrf';
function handler(req, res) {
const {
query: { page, lang, tag },
method,
} = req;
switch (method) {
case 'GET':
const posts = getPosts(page, lang, tag);
res.status(200).json(posts);
break;
default:
break;
}
}
export default csrf(handler);
There's also another thing happenig. If I try to call the api from postman the api works. I can see that there's a cookie with the "XSRF-TOKEN" value inside that I haven't set in any way, so I'm not sure where Postman is getting it:
How can I fix this?
Such error message is possible only in the case when cookie value is not a string. And according to next-csrf getCookie code the cookie value may be not a string, only in case when there are some cookies but not the required one.
And there is an error in next-csrf transpilation which makes code from line 52 to move to the line 26, skipping some checks and changing the program logic. You can see it here https://unpkg.com/browse/next-csrf#0.1.2/dist/next-csrf.js, at the line 1891.
Now:
To avoid this case you should send first request without any cookie set up. It seems like this is what Postman does and this is why it works in Postman.
A fix in the code of getCookie is required. The function should return string, not string or undefined.
Also I wouldn't recommend you to use this library without proper transpilation issue resolution.
Postman receives cookie, because next-csrf middleware defines set-cookie header, if there is no one in the first request.
I've been trying to configure my Application with an existing Backend as suggested in this link, from the amplifyConfig.ts configuration file (see below)
Then, I import the configuration file in the Login.tsx page and invoke it by passing Amplify.configure(currentConfig)
The first unexpected behavior is that, regardless I define authenticationFlowType: 'CUSTOM_AUTH', when I call the Auth.Signup method, the authenticationFlowType is still defined as "USR_SRP_AUTH".
Second, whenever I change the AmplifyConfig.ts file, I have to clear all the browsing data in order for those changes to work.
This behavior suggests me I'm doing something wrong, I understand I can deal this via cli, however I would prefer to handle this via code.
Thanks a lot!!
amplifyConfig.ts:
import Amplify, { Auth } from 'aws-amplify';
Amplify.configure({
Auth: {
// REQUIRED - Amazon Cognito Region
region: 'XX-XXXX-X',
// OPTIONAL - Amazon Cognito User Pool ID
userPoolId: 'XX-XXXX-X_abcd1234',
// OPTIONAL - Manually set the authentication flow type. Default is 'USER_SRP_AUTH'
authenticationFlowType: 'CUSTOM_AUTH',
}
}
});
// You can get the current config object
const currentConfig = Auth.configure();
Login.tsx:
import Amplify, { Auth } from 'aws-amplify';
import currentConfig from '../../services/amplifyConfig';
export function LoginMenu() {
Amplify.configure(currentConfig);
// redundant configuration of authenticationFlowType
Auth.configure({
authenticationFlowType: 'CUSTOM_AUTH',
});
async function onSignup(event: FormEvent) {
event?.preventDefault();
try {
const { user } = await Auth.signUp(UserData);
console.log(user);
} catch (error) {
console.log('error signing up:', error);
}
}
}
Here is my situation. I am trying to set up a Next.js project with an Express.js back-end. I did set up the back-end as a regular one not as a custom server according to Next.js documentation. So I am not sure if I am already making a mistake by setting up the back-end as a regular one. I am trying to fetch data from a back-end endpoint http://localhost:3500/api/v1/list using axios and it works well. But when I am trying to implement React-Query on the first load I am getting the right data from the back-end but when it is trying to re-fetch for some reason it is hitting the wrong end-point http://localhost:3600/api/v1/list and getting the 404 Not Found error. It looks like it is switching the port from 3500 to 3600 which is a front-end port. Here you will find the link to the repository and here is the code. Let me know if I am doing something wrong,
page/sell.js
import axios from 'axios';
import { useQuery } from 'react-query';
export default function SellPage({ response }) {
const { data, isLoading, error } = useQuery('sells', getPosts, {
initialData: response,
});
console.log('data useQuery: ', data);
if (isLoading) return 'Loading...';
if (error) return error.message;
return (
<div>
<p>Hello SellPage!!{data.message}</p>
</div>
);
}
export async function getServerSideProps(context) {
const response = await getPosts();
console.log('response', response);
if (!response) {
return {
notFound: true,
};
}
return {
props: { response }, // will be passed to the page component as props
};
}
async function getPosts() {
console.log('HOST: ', process.env.HOST);
const { data } = await axios.request({
baseURL: process.env.HOST,
url: '/api/v1/list',
method: 'get',
});
return data;
}
_app.js
import React from 'react';
import { QueryClient, QueryClientProvider } from 'react-query';
import { ReactQueryDevtools } from 'react-query/devtools';
const queryClient = new QueryClient();
function MyApp({ Component, pageProps }) {
const queryClientRef = React.useRef();
if (!queryClientRef.current) {
queryClientRef.current = new QueryClient();
}
return (
<>
<QueryClientProvider client={queryClientRef.current}>
<Component {...pageProps} />
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
</>
);
}
export default MyApp;
I don't see a next.config.js in your repo, so I guess the env variables are not bundled in the js and in the end you url looks like localhost: or localhost:undefined which the browser default to the port your client is served.
Try add next.config.js
module.exports = {
env: {
HOST: process.env.HOST,
},
}
SEE: https://nextjs.org/docs/api-reference/next.config.js/environment-variables
Another way is to use public runtime variables
module.exports = {
publicRuntimeConfig: {
// Will be available on both server and client
port: 3500,
},
};
// Then
import getConfig from 'next/config';
// Only holds serverRuntimeConfig and publicRuntimeConfig
const { publicRuntimeConfig } = getConfig();
console.log(publicRuntimeConfig.port);
SEE: https://nextjs.org/docs/api-reference/next.config.js/runtime-configuration
But note that runtime config would impact optimization and you might get larger bundle in the end so you might want to try build time variables first.
if you get 404
i think that you reached out the server but no API's name matched
so try to test the API first on postman or alike
however if console.log('HOST: ', process.env.HOST);
prints http://localhost:3600 then do the following
in your .env file try to rename PORT TO SERVER_PORT or whatever
HOSTNAME=localhost
SERVER_PORT=3500
HOST=http://$HOSTNAME:$SERVER_PORT
i'm not sure but maybe ur frontend serve bash hold PORT env val as 3600
I'm reconfiguring my NextJS/Apollo app to allow for SSG with GraphQL API routes, and I'm using this official NextJS starter example as a base for the client config.
I've run into an interesting issue though in my own app, so I've went back to starter example and tried to reproduce it, and was able to. The issue is that without any context object passed into the query resolvers, everything works fine (in the playground and on the client). However, when you introduce a context object and pass it to the resolvers, it works fine in the playground but the context object is undefined when fired from the client. This is the code from the official NextJS starter example, I'll comment where I've added anything.
graphql.js
import { ApolloServer } from "apollo-server-micro";
import { schema } from "../../apollo/schema";
const apolloServer = new ApolloServer({
schema,
context: { //
foo: "bar", // this is the context object I've added
}, //
});
export const config = {
api: {
bodyParser: false,
},
};
export default apolloServer.createHandler({ path: "/api/graphql" });
typedefs.js
import { gql } from '#apollo/client'
export const typeDefs = gql`
type User {
id: ID!
name: String!
status: String!
}
type Query {
viewer: User
}
`
schema.js
import { makeExecutableSchema } from 'graphql-tools'
import { typeDefs } from './type-defs'
import { resolvers } from './resolvers'
export const schema = makeExecutableSchema({
typeDefs,
resolvers,
})
resolvers.js
export const resolvers = {
Query: {
viewer: (_parent, _args, context, _info) => {
console.log("context", context); // console log check that I've added
return { id: 1, name: "John Smith", status: "cached" };
},
},
};
When I run this in the GraphQL playground and query the API, it gives me the correct response, and in my terminal console it returns the context foo: bar object from the console log, so in the server the context object is being passed correctly. However, when I visit the index page in the browser, which is this:
index.js
import gql from "graphql-tag";
import Link from "next/link";
import { useQuery } from "#apollo/client";
import { initializeApollo } from "../apollo/client";
const ViewerQuery = gql`
query ViewerQuery {
viewer {
id
name
status
}
}
`;
const Index = () => {
const {
data: { viewer },
} = useQuery(ViewerQuery);
return (
<div>
You're signed in as {viewer.name} and you're {viewer.status} goto{" "}
<Link href="/about">
<a>static</a>
</Link>{" "}
page.
</div>
);
};
export async function getStaticProps() {
const apolloClient = initializeApollo();
await apolloClient.query({
query: ViewerQuery,
});
return {
props: {
initialApolloState: apolloClient.cache.extract(),
},
};
}
export default Index;
...the viewer name and viewer status are rendered, so the query is actually happening, but in the console, the context object console log is returning undefined. So when used in the client, the context is being lost somehow. I find this interesting, since this is an official NextJS starter example, and unless they've set up the client to not accept context in the resolvers, I can't see what the problem is. And if it is the case that the client is not set up to accept context, is there any other official examples with a client setup that does?
This is a long question now, but here is the client.js setup:
import { useMemo } from "react";
import { ApolloClient, InMemoryCache } from "#apollo/client";
let apolloClient;
function createIsomorphLink() {
if (typeof window === "undefined") {
const { SchemaLink } = require("#apollo/client/link/schema");
const { schema } = require("./schema");
return new SchemaLink({ schema });
} else {
const { HttpLink } = require("#apollo/client/link/http");
return new HttpLink({
uri: "http://localhost:3000/api/graphql",
credentials: "same-origin",
});
}
}
function createApolloClient() {
return new ApolloClient({
ssrMode: typeof window === "undefined",
link: createIsomorphLink(),
cache: new InMemoryCache(),
});
}
export function initializeApollo(initialState = null) {
const _apolloClient = apolloClient ?? createApolloClient();
// If your page has Next.js data fetching methods that use Apollo Client, the initial state
// gets hydrated here
if (initialState) {
_apolloClient.cache.restore(initialState);
}
// For SSG and SSR always create a new Apollo Client
if (typeof window === "undefined") return _apolloClient;
// Create the Apollo Client once in the client
if (!apolloClient) apolloClient = _apolloClient;
return _apolloClient;
}
export function useApollo(initialState) {
const store = useMemo(() => initializeApollo(initialState), [initialState]);
return store;
}
I implore anyone to clone this official repo and see if they can figure out how to get context working in the client, or if anyone knows why this client setup isn't working for context and knows a client setup that does accept resolver context, I would appreciate it. This problem has cost me two days now!
I've figured out the problem. The client configuration is using SchemaLink for the http request, and the context is passed in the SchemaLink constructor function, not in the server options, because context is passed in the http headers with httpLink.
I'm following the tutorials at developers.sap.com for the Javascript:
Get Started with SAP Cloud SDK for JavaScript.
I created my application with:
sap-cloud-sdk init my-sdk-project
Now I'd like to add security to it, specifically, I want to use an approuter to access the app and I want to block any unauthenticated request to the service directly.
Optionally, I want to include scopes for the different endpoints of my app.
I don't have any problem adding an approuter, but when it comes to secure the node app, I can't seem to find the right way.
I can only find examples of securing an app with basic express node apps like these ones:
Hello World Sample using NodeJS
node.js Hello World
But they have a different structure that the one provided by sap-cloud-sdk tool, which uses nestjs.
The Help Portal doesn't point to any examplet either if you are using Nestjs.
Is there any resource, tutorial, or example to help me implement security in an scaffolded app?
Kr,
kepair
There is no resource yet on how to setup Cloud Foundry security with the Cloud SDK for JS, but I tinkered around with it a bit in the past with the following result.
Disclaimer: This is by no means production ready code! Please take this only as a inspiration and verify all behavior on your side via tests as well as adding robust error handling!
Introduce a scopes.decorator.ts file with the following content:
import { SetMetadata } from '#nestjs/common';
export const ScopesMetadataKey = 'scopes';
export const Scopes = (...scopes: string[]) => SetMetadata(ScopesMetadataKey, scopes);
This will create an annotation that you can add to your controller method in a follow up step. The parameters given will be the scopes that an endpoint requires before being called.
Create a Guard scopes.guard.ts like the following:
import { CanActivate, ExecutionContext, Injectable } from '#nestjs/common';
import { Reflector } from '#nestjs/core';
import { retrieveJwt, verifyJwt } from '#sap/cloud-sdk-core';
import { getServices } from '#sap/xsenv';
import { ScopesMetadataKey } from './scopes.decorator';
#Injectable()
export class ScopesGuard implements CanActivate {
private xsappname;
constructor(private readonly reflector: Reflector) {
this.xsappname = getServices({ uaa: { label: 'xsuaa' } }).uaa.xsappname;
}
async canActivate(context: ExecutionContext): Promise<boolean> {
const scopes = this.reflector.get<string[]>(ScopesMetadataKey, context.getHandler());
if (!scopes) {
return true;
}
const request = context.switchToHttp().getRequest();
const encodedJwt = retrieveJwt(request);
if (!encodedJwt) {
return false;
}
const jwt = await verifyJwt(encodedJwt);
return this.matchScopes(scopes, jwt.scope);
}
private matchScopes(expectedScopes: string[], givenScopes: string[]): boolean {
const givenSet = new Set(givenScopes);
return expectedScopes.every(scope => givenSet.has(this.xsappname + '.' + scope));
}
}
This Guard should be called before all endpoints and verifies that all requires scopes are present in the incoming JWT.
Add the guard to your nest application setup:
import { Reflector } from '#nestjs/core';
import { ScopesGuard } from './auth/scopes.guard';
// ...
const app = ...
const reflector = app.get(Reflector)
app.useGlobalGuards(new ScopesGuard(reflector));
// ...
This ensures that all incoming requests are actually "guarded" by your guard above.
Use the annotation created in the first step on your protection worthy endpoints:
import { Controller, Get } from '#nestjs/common';
import { Scopes } from '../auth/scopes.decorator';
#Controller('/api/rest/foo')
export class FooController {
constructor(private readonly fooService: FooService) {}
#Get()
#Scopes('FooViewer')
getFoos(): Promise<Foo[]> {
return this.fooService.getFoos();
}
}
This endpoint is now only callable if a JWT with the required scope is provided.
You can use the standard nodejs authentication implementation in sap-cloud-sdk/nest.js project without creating any middleware.
Since the JWTStrategy which is part of #sap/xssec have the middleware implementation, things are very simplified.
For Authentication change main.ts
import { NestFactory } from '#nestjs/core';
import { AppModule } from './app.module';
import { getServices } from '#sap/xsenv';
const xsuaa = getServices({ xsuaa: { tag: 'xsuaa' } }).xsuaa;
import * as passport from 'passport';
import { JWTStrategy } from '#sap/xssec';
passport.use(new JWTStrategy(xsuaa));
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.use(passport.initialize());
app.use(passport.authenticate('JWT', { session: false }));
await app.listen(process.env.PORT || 3000);
}
bootstrap();
This will initialize the middleware.
2. For scope check and authorization
import { Controller, Get, Req, HttpException, HttpStatus } from '#nestjs/common';
import { AppService } from './app.service';
#Controller()
export class AppController {
constructor(private readonly appService: AppService) { }
#Get()
getHello(#Req() req: any): any {
console.log(req.authInfo);
const isAuthorized = req.authInfo.checkLocalScope('YourScope');
if (isAuthorized) {
return req.user;
} else {
return new HttpException('Forbidden', HttpStatus.FORBIDDEN);
}
// return this.appService.getHello();
}
}
For more details please refer to this