I'd like to restrict users to be authenticated in Adonis.js, specifically to only authenticate users that have their active attribute set to true.
Route.post('login', async ({ auth, request, response }) => {
const { email, password } = request.only(['email', 'password'])
try {
const token = await auth.use('api').attempt(email, password)
return token
} catch {
return response.unauthorized('Invalid credentials')
}
})
I couldn't find a way to hook into the user retrieval by the auth module to add a constraint for active users. What would be a good approach to this?
Auth configuration:
const authConfig: AuthConfig = {
guard: 'api',
guards: {
api: {
driver: 'oat',
tokenProvider: {
type: 'api',
driver: 'database',
table: 'api_tokens',
foreignKey: 'user_id',
},
provider: {
driver: 'lucid',
identifierKey: 'id',
uids: ['email'],
model: () => import('App/Models/User'),
},
},
},
}
Related
I'm attempting to swap the default auth scheme in Blitz.js with a passport-azure-ad scheme, using the OIDCStrategy. I'm getting an error that I'm not sure about and would appreciate any help! I've created a new file under src/pages/auth/openid.tsx and into inserted the following code:
import { passportAuth } from "#blitzjs/auth"
import { api } from "src/blitz-server"
import { OIDCStrategy } from "passport-azure-ad"
const users: Array<{ oid: string }> = []
var findByOid = function (oid, fn) {
console.log("failing")
for (var i = 0, len = users.length; i < len; i++) {
const user = users[i]
console.log("we are using user: ", user)
if (user && user.oid === oid) {
return fn(null, user)
}
}
return fn(null, null)
}
export default api(
passportAuth({
successRedirectUrl: "/",
errorRedirectUrl: "/",
strategies: [
{
strategy: new OIDCStrategy(
{
identityMetadata:
"https://login.microsoftonline.com/<tenant-nam>.onmicrosoft.com/v2.0/.well-known/openid-configuration",
clientID: <client-id>,
responseType: "code id_token",
responseMode: "form_post",
redirectUrl: "http://localhost:3000/auth/openid/callback",
allowHttpForRedirectUrl: true,
clientSecret: "<client-secret>",
validateIssuer: false,
passReqToCallback: true,
scope: ["profile", "offline_access", "https://graph.microsoft.com/mail.read"],
loggingLevel: "info",
nonceMaxAmount: 5,
useCookieInsteadOfSession: false,
cookieEncryptionKeys: [
{ key: "12345678901234567890123456789012", iv: "123456789012" },
{ key: "abcdefghijklmnopqrstuvwxyzabcdef", iv: "abcdefghijkl" },
],
},
function (iss, sub, profile, accessToken, refreshToken, done) {
if (!profile.oid) {
return done(new Error("No oid found"), null)
}
// asynchronous verification, for effect...
process.nextTick(function () {
findByOid(profile.oid, function (err, user) {
if (err) {
return done(err)
}
if (!user) {
// "Auto-registration"
users.push(profile)
return done(null, profile)
}
return done(null, user)
})
})
}
),
},
],
})
)
I believe the configuration is good because I can run the example from passport-azure-ad from the github examples. The only change I make is that I set redirectUrl: "http://localhost:3000/auth/openid/callback", instead of redirectUrl: ".../return", per the blitz.js third party auth documentation. The tenantname, client_id, client_secret are redacted but I do set them to the correct values. I have also verified that the app registration is correctly set with the correct redirect uri.
I run blitz dev and when I go to the http://localhost:3000/auth/openid route I get the following error.
Here is the console output that is produced:
As you can see there is a Module not found: Can't resolve './src/build', this error only occurs if I go to the auth/openid page but the app is able to load.
I want to set login, logout callback url.
So, I set the callback url like this.
//signIn
const signInResult = await signIn("credentials", {
message,
signature,
redirect: false,
callbackUrl: `${env.nextauth_url}`,
});
//signOut
signOut({ callbackUrl: `${env.nextauth_url}`, redirect: false });
But, When I log in, I look at the network tab.
api/auth/providers, api/auth/callback/credentials? reply with
callbackUrl(url) localhost:3000
It's api/auth/callback/credentials? reply.
It's api/auth/providers reply
and api/auth/session reply empty object.
When I run on http://localhost:3000, everything was perfect.
But, After deploy, the login is not working properly.
How can I fix the error?
I added [...next-auth] code.
import CredentialsProvider from "next-auth/providers/credentials";
import NextAuth from "next-auth";
import Moralis from "moralis";
import env from "env.json";
export default NextAuth({
providers: [
CredentialsProvider({
name: "MoralisAuth",
credentials: {
message: {
label: "Message",
type: "text",
placeholder: "0x0",
},
signature: {
label: "Signature",
type: "text",
placeholder: "0x0",
},
},
async authorize(credentials: any): Promise<any> {
try {
const { message, signature } = credentials;
await Moralis.start({
apiKey: env.moralis_api_key,
});
const { address, profileId } = (
await Moralis.Auth.verify({ message, signature, network: "evm" })
).raw;
if (address && profileId) {
const user = { address, profileId, signature };
if (user) {
return user;
}
}
} catch (error) {
console.error(error);
return null;
}
},
}),
],
pages: {
signIn: "/",
signOut: "/",
},
session: {
maxAge: 3 * 24 * 60 * 60,
},
callbacks: {
async jwt({ token, user }) {
user && (token.user = user);
return token;
},
async session({ session, token }: any) {
session.user = token.user;
return session;
},
async redirect({ url, baseUrl }) {
// Allows relative callback URLs
if (url.startsWith("/")) return `${baseUrl}${url}`;
// Allows callback URLs on the same origin
else if (new URL(url).origin === baseUrl) return url;
return baseUrl;
},
},
secret: env.nextauth_secret,
});
I create a custom user registration because nextauth does not support registration. Everything works correctly but I do not know how after a successful registration of the user immediately log him in the credentials that he gave during registration.
As far as I can see, the signIn method from nextauth does not allow any credentials to be passed on to it. Redirects only to the login subpage.
I also did not find any endpoint that provides nextauth to which I can pass parameters so as to log in the user.
In fact, it is enough to call the authorize method that is in nextauth, unfortunately, there is no possibility to export it or there is no way to call it from the call to api level and it is a pity because with it I could log in the user.
User flow
User registers if the registration is successful, he is immediately logged in credentials that he provided during registration
My registration
async function handleRegister(
username: string,
email: string,
password: string
) {
const registerUser = await fetch(
`${process.env.API_URL}`,
{
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify({
username,
email,
password,
}),
}
);
const registerResponse = await registerUser.json();
if (registerResponse.user) {
// TODO: Login with NextAuth
}
}
[...nextauth].ts
export default NextAuth({
providers: [
CredentialsProvider({
name: "Credentials",
credentials: {
identifier: {
label: "Email or Username",
type: "text",
placeholder: "jsmith",
},
password: { label: "Password", type: "password" },
},
async authorize(credentials, req) {
const res = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/auth/local`,
{
method: "POST",
body: JSON.stringify(credentials),
headers: { "Content-Type": "application/json" },
}
);
const user = await res.json();
if (res.ok && user) {
return user;
}
return null;
},
}),
],
session: {
strategy: "jwt",
},
jwt: {
maxAge: 60,
encode: async ({ secret, token }) => {
const encodedToken = jsonwebtoken.sign(token!, secret, {
algorithm: "HS256",
});
return encodedToken;
},
decode: async ({ secret, token }) => {
const decodedToken = jsonwebtoken.verify(token!, secret, {
algorithms: ["HS256"],
});
return decodedToken as JWT;
},
},
callbacks: {
jwt: async (token, user, account) => {
const isSignIn = user ? true : false;
if (isSignIn) {
token.jwt = user.jwt;
token.id = user.user.id;
token.name = user.user.username;
token.role = user.user.user_role;
token.email = user.user.email;
}
return Promise.resolve(token);
},
session: async ({ session, token }) => {
if (session.user) {
session.user.id = token.sub!;
}
return session;
},
},
secret: process.env.JWT_SECRET,
});
You need to login with credentials e.g
const response: SignInResponse | undefined = await signIn(
"credentials",
{
redirect: false,
email: "example#user.com",
password: "12345678",
}
);
I'm using the Nuxt auth module v5 and the Laravel sanctum provider. My csrf-cookie route works fine, and my login route works fine, but when trying to call this.$axios from a function, such as when creating a user's account (since auth module doesn't offer this) I'm getting a CSRF token mismatch.
It would appear that using axios directly like this doesn't have access to setting the cookie since no user logged in, how can I get the cookie to be set?
Method for account creation
/*
** Create accounr
*/
createAccount () {
this.feedback.isShown = false
this.isCreatingAccount = true
if (this.apiAccountCreationSource) this.apiAccountCreationSource.cancel('aborted')
const CancelToken = this.$axios.CancelToken
this.apiAccountCreationSource = CancelToken.source()
this.$axios.post(`${this.$config.apiUrl}/api/account`, this.account, {
cancelToken: this.apiAccountCreationSource.token,
timeout: 30 * 1000
}).then(res => {
this.apiAccountCreationSource = null
this.setContextualResponse(res)
setTimeout(() => {
this.login()
}, 250)
}).catch(err => {
this.setContextualResponse(err.response ? err.response.data : null)
}).finally(() => {
this.isCreatingAccount = false
})
},
Nuxt config
// Axios module configuration: https://go.nuxtjs.dev/config-axios
axios: {
credentials: true,
baseURL: process.env.API_DOMAIN
},
// Auth module configuration: https://auth.nuxtjs.org/
auth: {
redirect: {
login: '/account/login/',
logout: '/account/login/',
callback: '/account/login/',
home: '/account/dashboard/'
},
strategies: {
'laravelSanctum': {
provider: 'laravel/sanctum',
url: process.env.API_DOMAIN,
endpoints: {
login: { url: '/api/login', method: 'post' },
logout: { url: '/api/account/logout', method: 'post' },
user: { url: '/api/account', method: 'get', propertyName: 'user' }
}
}
}
},
If you need to get the CSRF token all you need to do is make a request to your token endpoint and your browser should save the XSRF token. Then axios will automatically send this token in every subsequent request.
So all that you need to do is make a axios GET request to your csrf-cookie route before you send your POST request.
this.$axios.get(`${this.$config.apiUrl}/sanctum/csrf-cookie`)
Or you can chain both requests doing something like this:
this.$axios.get(`${this.$config.apiUrl}/sanctum/csrf-cookie`).then(() => {
return this.$axios.post(`${this.$config.apiUrl}/api/account`, this.account, {
cancelToken: this.apiAccountCreationSource.token,
timeout: 30 * 1000
}).then((res) => {
this.apiAccountCreationSource = null
this.setContextualResponse(res)
setTimeout(() => {
this.login()
}, 250)
}).catch((err) => {
this.setContextualResponse(err.response ? err.response.data : null)
}).finally(() => {
this.isCreatingAccount = false
})
})
Your authentication strategy works without this hassle because it handles this csrf request internally (https://github.com/nuxt-community/auth-module/blob/dev/src/providers/laravel-sanctum.ts)
References:
https://laravel.com/docs/8.x/sanctum#csrf-protection
https://github.com/axios/axios/issues/708#issuecomment-280920224
I have a GraphQL endpoint:
app.use('/graphql', graphqlHTTP(request => ({
graphiql: true,
schema
})));
I also have a Passport route for logging in (and handling the callback, since I'm using Google OAuth2):
this.app.get('/login', passport.authenticate('google'));
this.app.get('/auth/callback/google', ....
Passport add a user to the request, and all of the articles I can find online recommend authenticating in each of my GraphQL resolvers using that:
resolve: (root, args, { user }) => {
if (!user) throw new NotLoggedInError();
However it doesn't make sense to have to add that logic to every resolver when it applies to all of them, so I was hoping to somehow authenticate the entire endpoint.
The problem is that I'm not sure how to combine middleware. I tried the following but it just broke the endpoint:
app.use('/graphql', passport.authenticate('google'), graphqlHTTP(request => ({
graphiql: true,
schema
})));
I have the following working. Some issues I had were around making sure my google API was enabled and the proper scopes were enabled. I am also only using the passport middleware on the auth endpoints and using an isAuthenticated middleware to check if the session is authenticated and if not redirect to the auth endpoint. also putting the request object into the context so that it can be used by the resolver to potentially authorize the user. You would of course need to update the user lookup as I am just passing mock data.
import express from "express";
import graphqlHTTP from "express-graphql";
import passport from "passport";
import cookieParser from "cookie-parser";
import session from "express-session";
import { Strategy as GoogleStrategy } from "passport-google-oauth20";
import { buildSchema } from "graphql";
const PORT = 5000;
const data = [
{ id: "1", name: "foo1" },
{ id: "2", name: "foo2" },
{ id: "3", name: "foo3" },
];
const def = `
type Foo {
id: String!
name: String
}
type Query {
readFoo(id: String!): Foo
}
schema {
query: Query
}
`;
const schema = buildSchema(def);
const fieldMap = schema.getType("Query").getFields();
fieldMap.readFoo.resolve = (source, args) => {
return data.filter(({ id }) => id === args.id)[0] || null;
};
passport.serializeUser((user, done) => {
done(null, user);
});
passport.deserializeUser((obj, done) => {
done(null, obj);
});
passport.use(
new GoogleStrategy(
{
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: `http://localhost:${PORT}/auth/google/callback`,
},
(accessToken, refreshToken, profile, cb) => {
return cb(null, {
id: "1",
username: "foo#bar.baz",
googleId: profile.id,
});
}
)
);
function isAuthenticated(req, res, next) {
return req.isAuthenticated() ? next() : res.redirect("/auth/google");
}
const app = express();
app.use(cookieParser());
app.use(
session({
secret: "sauce",
resave: false,
saveUninitialized: false,
})
);
app.use(passport.initialize());
app.use(passport.session());
app.get("/auth/fail", (req, res) => {
res.json({ loginFailed: true });
});
app.get(
"/auth/google",
passport.authenticate("google", { scope: ["profile"] })
);
app.get(
"/auth/google/callback",
passport.authenticate("google", { failureRedirect: "/auth/fail" }),
(req, res) => {
res.redirect("/graphql");
}
);
app.use(
"/graphql",
isAuthenticated,
graphqlHTTP((req) => ({
schema,
graphiql: true,
context: req,
}))
);
app.listen(PORT, () => {
console.log("Started local graphql server on port ", PORT);
});
vbranden's answer was excellent, and it is the basis of this answer. However, his answer has a lot of other code which obfuscates the solution a bit. I didn't want to mess with it, since it offers a more complete view of things, but hopefully this answer will be helpful in its own way by being more direct. But again, all credit for this solution belongs to vbranden (please upvote his answer accordingly).
If you make an isAuthenticated function with the appropriate signature (request, response, next) you can then "chain" that function in when you setup your GraphQL endpoint:
function isAuthenticated(req, res, next) {
return req.isAuthenticated() ?
next() :
res.redirect('/auth/google');
}
app.use(
'/graphql',
isAuthenticated,
graphqlHTTP(req => ({
schema,
graphiql: true,
context: req
}))
);