API Authentication

Is there a good example of how to add an api endpoint for authentication? I'm primarily using username and password for now but eventually I'd like make use of all the authentication methods provided by wasp, but through an api.
27 Replies
kapa.ai
kapa.ai6mo ago
Hi @JLegendz. I'm kapa.ai, your friendly support bot. I will try to answer your question by searching through the knowledge base. :wave: ---------------------- Wasp provides authentication and authorization support out of the box, including username and password authentication. However, it doesn't directly provide an API endpoint for authentication. Instead, it provides a mechanism to create custom API endpoints and use them in your application. Here's a basic example of how you can create a custom API endpoint:
// main.wasp
api fooBar {
fn: import { fooBar } from "@src/apis",
httpRoute: (GET, "/foo/bar")
}
// main.wasp
api fooBar {
fn: import { fooBar } from "@src/apis",
httpRoute: (GET, "/foo/bar")
}
// src/apis.js
export const fooBar = (req, res, context) => {
res.set("Access-Control-Allow-Origin", "*");
res.json({ msg: `Hello, ${context.user ? "registered user" : "stranger"}!` });
};
// src/apis.js
export const fooBar = (req, res, context) => {
res.set("Access-Control-Allow-Origin", "*");
res.json({ msg: `Hello, ${context.user ? "registered user" : "stranger"}!` });
};
In this example, fooBar is a custom API endpoint that sends a different message based on whether the user is authenticated or not. The context.user object is provided by Wasp and contains user session information if the user is authenticated. To use this API from your client, you can import the Axios wrapper from wasp/client/api and invoke a call. For example:
import { api } from "wasp/client/api";

async function fetchCustomRoute() {
const res = await api.get("/foo/bar");
console.log(res.data);
}
import { api } from "wasp/client/api";

async function fetchCustomRoute() {
const res = await api.get("/foo/bar");
console.log(res.data);
}
This will call the fooBar API endpoint and log the response. For authentication, you can enable it in your app declaration in the Wasp file:
app MyApp {
title: "My app",
auth: {
userEntity: User,
methods: {
usernameAndPassword: {},
// other auth methods...
},
onAuthFailedRedirectTo: "/someRoute"
}
}
app MyApp {
title: "My app",
auth: {
userEntity: User,
methods: {
usernameAndPassword: {},
// other auth methods...
},
onAuthFailedRedirectTo: "/someRoute"
}
}
In this example, usernameAndPassword: {} enables username and password authentication. You can add other authentication methods in the methods field. Please note that this is a simplified example and you might need to adjust it based on your requirements. For more details, you can refer to the Wasp documentation (https://wasp-lang.dev/docs/advanced/apis). Sources: - Custom HTTP API Endpoints (https://wasp-lang.dev/docs/advanced/apis) - Auth Overview (https://wasp-lang.dev/docs/auth/overview) - Username & Password Authentication (https://wasp-lang.dev/docs/auth/username-and-pass) ---------------------- Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: ---------------------- Sorry @JLegendz, I seem to be having a bit of trouble. Don't worry though, I've pinged my support and they will try to fix me up! :technologist: Perhaps try again in 15mins?
JLegendz
JLegendzOP6mo ago
@kapa.ai are there examples of how to setup a username and password endpoint?
kapa.ai
kapa.ai6mo ago
Let me check the knowledge base for you. One moment! :books: ---------------------- Yes, Wasp provides a built-in mechanism for setting up username and password authentication. Here's a step-by-step guide on how to set it up: 1. Enable Username Authentication: Add the following to your main.wasp file to enable username and password authentication:
app myApp {
wasp: {
version: "^0.13.0"
},
title: "My App",
auth: {
// 1. Specify the user entity (we'll define it next)
userEntity: User,
methods: {
// 2. Enable username authentication
usernameAndPassword: {},
},
onAuthFailedRedirectTo: "/login"
}
}
app myApp {
wasp: {
version: "^0.13.0"
},
title: "My App",
auth: {
// 1. Specify the user entity (we'll define it next)
userEntity: User,
methods: {
// 2. Enable username authentication
usernameAndPassword: {},
},
onAuthFailedRedirectTo: "/login"
}
}
2. Add the User Entity: You need to define a User entity in your main.wasp file. Here's an example of how to do it:
entity User {=psl
id Int @id @default(autoincrement())
tasks Task[]
psl=}
entity User {=psl
id Int @id @default(autoincrement())
tasks Task[]
psl=}
3. Add the Auth Routes and Pages: Define the routes and pages for the authentication pages in your main.wasp file:
// 4. Define the routes
route LoginRoute { path: "/login", to: LoginPage }
page LoginPage {
component: import { Login } from "@src/pages/auth.jsx"
}
route SignupRoute { path: "/signup", to: SignupPage }
page SignupPage {
component: import { Signup } from "@src/pages/auth.jsx"
}
// 4. Define the routes
route LoginRoute { path: "/login", to: LoginPage }
page LoginPage {
component: import { Login } from "@src/pages/auth.jsx"
}
route SignupRoute { path: "/signup", to: SignupPage }
page SignupPage {
component: import { Signup } from "@src/pages/auth.jsx"
}
4. Use Auth UI Components in Pages: Create a auth.tsx file in the src/pages folder and add the following to it:
import { LoginForm, SignupForm } from 'wasp/client/auth'
import { Link } from 'react-router-dom'

export function Login() {
return (
<Layout>
<LoginForm />
<br />
<span className="text-sm font-medium text-gray-900">
Don't have an account yet? <Link to="/signup">go to signup</Link>.
</span>
</Layout>
);
}

export function Signup() {
return (
<Layout>
<SignupForm />
<br />
<span className="text-sm font-medium text-gray-900">
I already have an account (<Link to="/login">go to login</Link>).
</span>
</Layout>
);
}
import { LoginForm, SignupForm } from 'wasp/client/auth'
import { Link } from 'react-router-dom'

export function Login() {
return (
<Layout>
<LoginForm />
<br />
<span className="text-sm font-medium text-gray-900">
Don't have an account yet? <Link to="/signup">go to signup</Link>.
</span>
</Layout>
);
}

export function Signup() {
return (
<Layout>
<SignupForm />
<br />
<span className="text-sm font-medium text-gray-900">
I already have an account (<Link to="/login">go to login</Link>).
</span>
</Layout>
);
}
For more details, you can refer to the Wasp documentation on Username & Password Authentication (https://wasp-lang.dev/docs/auth/username-and-pass). ---------------------- Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: ---------------------- Sorry @JLegendz, I seem to be having a bit of trouble. Don't worry though, I've pinged my support and they will try to fix me up! :technologist: Perhaps try again in 15mins?
JLegendz
JLegendzOP6mo ago
@kapa.ai I want to authenticate from postman, not from inside the app directly.
kapa.ai
kapa.ai6mo ago
Let me check the knowledge base for you. One moment! :books: ---------------------- To authenticate from Postman or any other external tool, you would typically send a request to the authentication endpoint of your application with the necessary credentials. However, Wasp currently doesn't provide a direct API endpoint for authentication that you can use from Postman or similar tools. That said, you can create custom API endpoints in Wasp and handle authentication manually in those endpoints. Here's a simplified example of how you might do it:
// main.wasp
api login {{
fn: import {{ login }} from "@src/apis",
httpRoute: (POST, "/login")
}}
// main.wasp
api login {{
fn: import {{ login }} from "@src/apis",
httpRoute: (POST, "/login")
}}
// src/apis.js
import bcrypt from 'bcrypt'
import jwt from 'jsonwebtoken'
import { PrismaClient } from '@prisma/client'

const prisma = new PrismaClient()

export const login = async (req, res) => {{
const { username, password } = req.body

const user = await prisma.user.findUnique({ where: { username } })
if (!user) {{
res.status(401).json({ message: 'No such user found' })
return
}}

const valid = await bcrypt.compare(password, user.password)
if (!valid) {{
res.status(401).json({ message: 'Incorrect password' })
return
}}

const token = jwt.sign({ id: user.id }, 'your-secret-key')
res.json({ token })
}}
// src/apis.js
import bcrypt from 'bcrypt'
import jwt from 'jsonwebtoken'
import { PrismaClient } from '@prisma/client'

const prisma = new PrismaClient()

export const login = async (req, res) => {{
const { username, password } = req.body

const user = await prisma.user.findUnique({ where: { username } })
if (!user) {{
res.status(401).json({ message: 'No such user found' })
return
}}

const valid = await bcrypt.compare(password, user.password)
if (!valid) {{
res.status(401).json({ message: 'Incorrect password' })
return
}}

const token = jwt.sign({ id: user.id }, 'your-secret-key')
res.json({ token })
}}
In this example, login is a custom API endpoint that handles username and password authentication. It uses bcrypt to compare the provided password with the hashed password stored in the database, and if the password is correct, it generates a JWT token and sends it in the response. You can then use this token to authenticate subsequent requests from Postman by including it in the Authorization header. Please note that this is a simplified example and you might need to adjust it based on your requirements. For more details, you can refer to the Wasp documentation (https://wasp-lang.dev/docs/advanced/apis). Sources: - Custom HTTP API Endpoints (https://wasp-lang.dev/docs/advanced/apis) - Auth Overview (https://wasp-lang.dev/docs/auth/overview) - Username & Password Authentication (https://wasp-lang.dev/docs/auth/username-and-pass) ---------------------- Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: ---------------------- Sorry @JLegendz, I seem to be having a bit of trouble. Don't worry though, I've pinged my support and they will try to fix me up! :technologist: Perhaps try again in 15mins?
JLegendz
JLegendzOP6mo ago
@kapa.ai is the user.password actually present for comparison?
kapa.ai
kapa.ai6mo ago
Let me check the knowledge base for you. One moment! :books: ---------------------- In the Wasp framework, the user's password is not directly accessible for comparison. The password is hashed before it is stored in the database for security reasons. This means you cannot retrieve the original password from the database. However, in the context of the custom login API endpoint example I provided earlier, the user.password refers to the hashed password stored in the database. The bcrypt.compare() function is used to compare the plaintext password provided in the login request with the hashed password. This function internally hashes the provided password with the same salt used for the stored password and then compares the two hashes. Please note that this example assumes that you are manually handling password hashing and storage in your custom API endpoint, which is not the typical usage of Wasp. In the standard Wasp authentication flow, password hashing and comparison are handled automatically by the framework. For more details, you can refer to the Wasp documentation (https://wasp-lang.dev/docs/advanced/apis). Sources: - Custom HTTP API Endpoints (https://wasp-lang.dev/docs/advanced/apis) - Auth Overview (https://wasp-lang.dev/docs/auth/overview) - Username & Password Authentication (https://wasp-lang.dev/docs/auth/username-and-pass) ---------------------- Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: ---------------------- Sorry @JLegendz, I seem to be having a bit of trouble. Don't worry though, I've pinged my support and they will try to fix me up! :technologist: Perhaps try again in 15mins?
JLegendz
JLegendzOP6mo ago
@kapa.ai I get an error when trying to access user.password in this matter.
[ Wasp ] ext-src/apis.ts(20,57): error TS2339: Property 'password' does not exist on type 'GetResult<{ id: number; createdAt: Date; email: string; username: string; lastActiveTimestamp: Date; isAdmin: boolean; stripeId: string; checkoutSessionId: string; subscriptionTier: string; subscriptionStatus: string; sendEmail: boolean; datePaid: Date; credits: number; }, unknown> & {}'.
[ Wasp ] ext-src/apis.ts(20,57): error TS2339: Property 'password' does not exist on type 'GetResult<{ id: number; createdAt: Date; email: string; username: string; lastActiveTimestamp: Date; isAdmin: boolean; stripeId: string; checkoutSessionId: string; subscriptionTier: string; subscriptionStatus: string; sendEmail: boolean; datePaid: Date; credits: number; }, unknown> & {}'.
kapa.ai
kapa.ai6mo ago
Let me check the knowledge base for you. One moment! :books: ---------------------- The error message you're seeing indicates that the password field does not exist on the user object. This is expected behavior in Wasp, as the framework automatically hashes passwords and does not include them when fetching user data from the database for security reasons. If you need to authenticate a user, you should use the built-in authentication methods provided by Wasp, which handle password hashing and comparison for you. If you're trying to create a custom login endpoint, you would typically not compare the passwords directly. Instead, you would use a function provided by Wasp or a library like bcrypt to validate the provided password. Here's an example of how you might create a custom login function using Wasp's built-in functions:
import {
createProviderId,
findAuthIdentity,
updateAuthIdentityProviderData,
deserializeAndSanitizeProviderData,
} from 'wasp/server/auth';

export const login = async (args, context) => {
const providerId = createProviderId('email', args.email)
const authIdentity = await findAuthIdentity(providerId)
if (!authIdentity) {
throw new HttpError(400, "Unknown user")
}

const providerData = deserializeAndSanitizeProviderData(authIdentity.providerData)

// Validate the password.
if (!providerData.hashedPassword || !await bcrypt.compare(args.password, providerData.hashedPassword)) {
throw new HttpError(400, "Incorrect password")
}

// User is authenticated, return user data or token as needed.
}
import {
createProviderId,
findAuthIdentity,
updateAuthIdentityProviderData,
deserializeAndSanitizeProviderData,
} from 'wasp/server/auth';

export const login = async (args, context) => {
const providerId = createProviderId('email', args.email)
const authIdentity = await findAuthIdentity(providerId)
if (!authIdentity) {
throw new HttpError(400, "Unknown user")
}

const providerData = deserializeAndSanitizeProviderData(authIdentity.providerData)

// Validate the password.
if (!providerData.hashedPassword || !await bcrypt.compare(args.password, providerData.hashedPassword)) {
throw new HttpError(400, "Incorrect password")
}

// User is authenticated, return user data or token as needed.
}
In this example, login is a custom function that uses Wasp's built-in functions to fetch the user's hashed password from the database and validate the provided password. Please note that this is a simplified example and you might need to adjust it based on your requirements. For more details, you can refer to the Wasp documentation (https://wasp-lang.dev/docs/advanced/apis). Sources: - Custom HTTP API Endpoints (https://wasp-lang.dev/docs/advanced/apis) - Auth Overview (https://wasp-lang.dev/docs/auth/overview) - Username & Password Authentication (https://wasp-lang.dev/docs/auth/username-and-pass) ---------------------- Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: ---------------------- Sorry @JLegendz, I seem to be having a bit of trouble. Don't worry though, I've pinged my support and they will try to fix me up! :technologist: Perhaps try again in 15mins?
JLegendz
JLegendzOP6mo ago
@kapa.ai Why would bcrypt say false when comparing the passed in password with the hashed Password. I know the password is correct. Should the password being passed in already be hashed? Currently it's just the exact password being passed in.
MEE6
MEE66mo ago
Wohooo @JLegendz, you just became a Waspeteer level 4!
kapa.ai
kapa.ai6mo ago
Let me check the knowledge base for you. One moment! :books: ---------------------- The bcrypt.compare() function is designed to take a plaintext password as the first argument and a hashed password as the second argument. It hashes the plaintext password with the salt from the hashed password and then compares the two hashes. If they match, it returns true; otherwise, it returns false. If bcrypt.compare() is returning false even though you're sure the password is correct, there could be a few reasons: 1. Incorrect Hash: The hashed password stored in the database might not be correct. This could happen if there was an error when the password was hashed and stored. 2. Different Salt: Each time you hash a password with bcrypt, a new salt is generated. If you're hashing the password again before comparing, a different salt will be used, resulting in a different hash. 3. Password Modification: The password might have been modified after it was hashed (for example, by trimming whitespace). This would cause the hash of the modified password to be different from the stored hash. Here's an example of how you should use bcrypt.compare():
const isValidPassword = await bcrypt.compare(plainTextPassword, hashedPassword);
const isValidPassword = await bcrypt.compare(plainTextPassword, hashedPassword);
In this example, plainTextPassword is the password provided by the user and hashedPassword is the hashed password fetched from the database. If you're still having issues, you might want to check the value of the hashed password stored in the database and ensure that your passwords are being hashed correctly when they are stored. Sources: - Auth Overview (https://wasp-lang.dev/docs/auth/overview) - Username & Password Authentication (https://wasp-lang.dev/docs/auth/username-and-pass) ---------------------- Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: ---------------------- Sorry @JLegendz, I seem to be having a bit of trouble. Don't worry though, I've pinged my support and they will try to fix me up! :technologist: Perhaps try again in 15mins?
JLegendz
JLegendzOP6mo ago
@kapa.ai Everything seems to work fine with this, except bcrypt.compare is returning false when the password is correct:
import { MiddlewareConfigFn, HttpError } from "wasp/server";
import { FooBar, GetListings, GetPromotions, GetListing, GetPromotion } from "wasp/server/api"; // This type is generated by Wasp based on the `api` declaration above.
import bcrypt from 'bcryptjs'
import { Request, Response } from 'express';
import { EmailProviderData } from 'wasp/server/auth'; // Add this import
import {

createProviderId,
findAuthIdentity,
updateAuthIdentityProviderData,
deserializeAndSanitizeProviderData,
} from 'wasp/server/auth';




export const login = async (req: Request, res: Response) => {
const { email, password } = req.body;


const providerId = createProviderId('email', email);
const authIdentity = await findAuthIdentity(providerId);

if (!authIdentity) {
throw new HttpError(400, "Unknown user");
}

const providerData = deserializeAndSanitizeProviderData(authIdentity.providerData);

// Validate the password.
const isValidPassword = await bcrypt.compare(password, (providerData as EmailProviderData).hashedPassword);

if (!isValidPassword) {
throw new HttpError(400, "Incorrect password");
} else {
console.log("CORRECT PASSWORD!");
}

// User is authenticated, return user data or token as needed.
}
import { MiddlewareConfigFn, HttpError } from "wasp/server";
import { FooBar, GetListings, GetPromotions, GetListing, GetPromotion } from "wasp/server/api"; // This type is generated by Wasp based on the `api` declaration above.
import bcrypt from 'bcryptjs'
import { Request, Response } from 'express';
import { EmailProviderData } from 'wasp/server/auth'; // Add this import
import {

createProviderId,
findAuthIdentity,
updateAuthIdentityProviderData,
deserializeAndSanitizeProviderData,
} from 'wasp/server/auth';




export const login = async (req: Request, res: Response) => {
const { email, password } = req.body;


const providerId = createProviderId('email', email);
const authIdentity = await findAuthIdentity(providerId);

if (!authIdentity) {
throw new HttpError(400, "Unknown user");
}

const providerData = deserializeAndSanitizeProviderData(authIdentity.providerData);

// Validate the password.
const isValidPassword = await bcrypt.compare(password, (providerData as EmailProviderData).hashedPassword);

if (!isValidPassword) {
throw new HttpError(400, "Incorrect password");
} else {
console.log("CORRECT PASSWORD!");
}

// User is authenticated, return user data or token as needed.
}
kapa.ai
kapa.ai6mo ago
Let me check the knowledge base for you. One moment! :books: ---------------------- Your code seems correct and should work as expected. The bcrypt.compare() function is used correctly: it takes the plaintext password and the hashed password, hashes the plaintext password with the salt from the hashed password, and then compares the two hashes. If bcrypt.compare() is returning false even though the password is correct, there could be a few reasons: 1. Incorrect Hash: The hashed password stored in the database might not be correct. This could happen if there was an error when the password was hashed and stored. 2. Different Salt: Each time you hash a password with bcrypt, a new salt is generated. If you're hashing the password again before comparing, a different salt will be used, resulting in a different hash. 3. Password Modification: The password might have been modified after it was hashed (for example, by trimming whitespace). This would cause the hash of the modified password to be different from the stored hash. Here are a few debugging steps you can take: - Check the hashed password: Print out the hashed password stored in the database and the plaintext password that you're trying to compare. Make sure they are what you expect. - Check the bcrypt version: There could be compatibility issues between different versions of bcrypt. Make sure you're using a version of bcrypt that is compatible with the way the passwords were hashed. - Try a known password: Hash a known password, store it in the database, and then try to authenticate with that password. This can help you determine if the issue is with the hashing process or the comparison process. If you're still having issues, you might want to ask for help on the Wasp community forum (https://discourse.wasp-lang.dev/) or GitHub issues (https://github.com/wasp-lang/wasp/issues). Sources: - Auth Overview (https://wasp-lang.dev/docs/auth/overview) - Username & Password Authentication (https://wasp-lang.dev/docs/auth/username-and-pass) ---------------------- Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: ---------------------- Sorry @JLegendz, I seem to be having a bit of trouble. Don't worry though, I've pinged my support and they will try to fix me up! :technologist: Perhaps try again in 15mins?
JLegendz
JLegendzOP6mo ago
@kapa.ai How can I confirm that bcryptjs is compatible with the way that passwords are hashed by wasp?
kapa.ai
kapa.ai6mo ago
Let me check the knowledge base for you. One moment! :books: ---------------------- Wasp uses the secure-password package for password hashing, as mentioned in the migration guide (https://wasp-lang.dev/docs/migrate-from-0-11-to-0-12#username--password). This package uses the same underlying algorithm as bcrypt (bcrypt uses Blowfish cipher algorithm), but it's not exactly the same as bcrypt or bcryptjs. To confirm compatibility, you can try hashing a password using secure-password and then compare it using bcrypt.compare(). However, due to the differences in the way these libraries handle hashing, this might not work as expected. Here's an example of how you might do this:
import SecurePassword from 'secure-password'
import bcrypt from 'bcryptjs'

const pwd = SecurePassword()
const password = 'my-password'

// Hash the password using secure-password
const hashedPassword = await pwd.hash(Buffer.from(password))

// Compare using bcrypt
const isValidPassword = await bcrypt.compare(password, hashedPassword.toString('hex'))
console.log(isValidPassword) // This might not work as expected
import SecurePassword from 'secure-password'
import bcrypt from 'bcryptjs'

const pwd = SecurePassword()
const password = 'my-password'

// Hash the password using secure-password
const hashedPassword = await pwd.hash(Buffer.from(password))

// Compare using bcrypt
const isValidPassword = await bcrypt.compare(password, hashedPassword.toString('hex'))
console.log(isValidPassword) // This might not work as expected
If bcrypt.compare() is not able to correctly verify the password hashed by secure-password, it means they are not compatible. In this case, if you need to verify a password outside of the Wasp application (for example, in a separate service or in a testing tool like Postman), you would need to use the same secure-password library that Wasp uses for password hashing. Sources: - Migration from 0.11.X to 0.12.X (https://wasp-lang.dev/docs/migrate-from-0-11-to-0-12#username--password) - Auth Overview (https://wasp-lang.dev/docs/auth/overview) - Username & Password Authentication (https://wasp-lang.dev/docs/auth/username-and-pass) ---------------------- Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: ---------------------- Sorry @JLegendz, I seem to be having a bit of trouble. Don't worry though, I've pinged my support and they will try to fix me up! :technologist: Perhaps try again in 15mins?
JLegendz
JLegendzOP6mo ago
So far I'm pretty stomped here. Any suggestions? How do I handle username and password to login via an api? @kapa.ai now that we've established that I should use secure-password instead of bcrypt, what should my login api in api.ts look like?
kapa.ai
kapa.ai6mo ago
Let me check the knowledge base for you. One moment! :books: ---------------------- Sure, you can use the secure-password library to verify the password in your custom login API. Here's how you might modify your login function:
import { MiddlewareConfigFn, HttpError } from "wasp/server";
import { Request, Response } from 'express';
import { EmailProviderData } from 'wasp/server/auth'; // Add this import
import {
createProviderId,
findAuthIdentity,
deserializeAndSanitizeProviderData,
} from 'wasp/server/auth';
import SecurePassword from 'secure-password';

const pwd = SecurePassword();

export const login = async (req: Request, res: Response) => {
const { email, password } = req.body;

const providerId = createProviderId('email', email);
const authIdentity = await findAuthIdentity(providerId);

if (!authIdentity) {
throw new HttpError(400, "Unknown user");
}

const providerData = deserializeAndSanitizeProviderData(authIdentity.providerData);

// Validate the password.
const passwordValidationResult = pwd.verify(Buffer.from(password), Buffer.from((providerData as EmailProviderData).hashedPassword, 'base64'));

if (passwordValidationResult !== SecurePassword.VALID) {
throw new HttpError(400, "Incorrect password");
} else {
console.log("CORRECT PASSWORD!");
}

// User is authenticated, return user data or token as needed.
}
import { MiddlewareConfigFn, HttpError } from "wasp/server";
import { Request, Response } from 'express';
import { EmailProviderData } from 'wasp/server/auth'; // Add this import
import {
createProviderId,
findAuthIdentity,
deserializeAndSanitizeProviderData,
} from 'wasp/server/auth';
import SecurePassword from 'secure-password';

const pwd = SecurePassword();

export const login = async (req: Request, res: Response) => {
const { email, password } = req.body;

const providerId = createProviderId('email', email);
const authIdentity = await findAuthIdentity(providerId);

if (!authIdentity) {
throw new HttpError(400, "Unknown user");
}

const providerData = deserializeAndSanitizeProviderData(authIdentity.providerData);

// Validate the password.
const passwordValidationResult = pwd.verify(Buffer.from(password), Buffer.from((providerData as EmailProviderData).hashedPassword, 'base64'));

if (passwordValidationResult !== SecurePassword.VALID) {
throw new HttpError(400, "Incorrect password");
} else {
console.log("CORRECT PASSWORD!");
}

// User is authenticated, return user data or token as needed.
}
In this example, login is a custom function that uses Wasp's built-in functions to fetch the user's hashed password from the database and validate the provided password using secure-password. Please note that this is a simplified example and you might need to adjust it based on your requirements. For more details, you can refer to the Wasp documentation (https://wasp-lang.dev/docs/advanced/apis). Sources: - Custom HTTP API Endpoints (https://wasp-lang.dev/docs/advanced/apis) - Auth Overview (https://wasp-lang.dev/docs/auth/overview) - Username & Password Authentication (https://wasp-lang.dev/docs/auth/username-and-pass) ---------------------- Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: ---------------------- Sorry @JLegendz, I seem to be having a bit of trouble. Don't worry though, I've pinged my support and they will try to fix me up! :technologist: Perhaps try again in 15mins?
JLegendz
JLegendzOP6mo ago
@kapa.ai unfortunately findAuthIdentity always returns null, even when passed the username that I know exists
kapa.ai
kapa.ai6mo ago
Let me check the knowledge base for you. One moment! :books: ---------------------- The findAuthIdentity function is used to find a specific authentication identity based on the providerId. The providerId is created using the createProviderId function, which takes two arguments: the provider name and the provider user ID. If findAuthIdentity is returning null, it means that it couldn't find an authentication identity that matches the provided providerId. This could happen for a few reasons: 1. Incorrect Provider Name: The provider name you're using to create the providerId might be incorrect. Make sure you're using the correct provider name. For example, if you're using email authentication, the provider name should be 'email'. 2. Incorrect Provider User ID: The provider user ID you're using to create the providerId might be incorrect. Make sure you're using the correct provider user ID. For example, if you're using email authentication, the provider user ID should be the user's email. 3. User Doesn't Exist: The user might not exist in the database. Make sure the user exists and has an authentication identity that matches the providerId. Here's an example of how you might use findAuthIdentity:
const providerId = createProviderId('email', email);
const authIdentity = await findAuthIdentity(providerId);
const providerId = createProviderId('email', email);
const authIdentity = await findAuthIdentity(providerId);
In this example, email is the email of the user you're trying to authenticate. If you're still having issues, you might want to check the value of the providerId and ensure that the user exists in the database. Sources: - Auth Overview (https://wasp-lang.dev/docs/auth/overview) - Username & Password Authentication (https://wasp-lang.dev/docs/auth/username-and-pass) - Migration from 0.11.X to 0.12.X (https://wasp-lang.dev/docs/migrate-from-0-11-to-0-12#username--password) ---------------------- Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: ---------------------- Sorry @JLegendz, I seem to be having a bit of trouble. Don't worry though, I've pinged my support and they will try to fix me up! :technologist: Perhaps try again in 15mins?
JLegendz
JLegendzOP6mo ago
@kapa.ai I've made the suggested correction and findAuthIdentity now returns the user's provider data. However, now I get the following error:
[ Server ] POST /login 500 14.525 ms - 930
[ Server!] Error: hashBuf must be HASH_BYTES (128)
[ Server!] at assert (/home/jparker/my_apps/sass_one/advertiser/app/node_modules/nanoassert/index.js:21:17)
[ Server!] at SecurePassword.verify (/home/jparker/my_apps/sass_one/advertiser/app/node_modules/secure-password/index.js:126:3)
[ Server!] at /home/jparker/my_apps/sass_one/advertiser/app/node_modules/secure-password/index.js:109:12
[ Server!] at new Promise (<anonymous>)
[ Server!] at SecurePassword.verify (/home/jparker/my_apps/sass_one/advertiser/app/node_modules/secure-password/index.js:108:12)
[ Server!] at login (/home/jparker/my_apps/sass_one/advertiser/app/src/apis.ts:34:48)
[ Server!] at <anonymous> (/home/jparker/my_apps/sass_one/advertiser/app/.wasp/out/sdk/wasp/server/utils.ts:26:5)
[ Server ] POST /login 500 14.525 ms - 930
[ Server!] Error: hashBuf must be HASH_BYTES (128)
[ Server!] at assert (/home/jparker/my_apps/sass_one/advertiser/app/node_modules/nanoassert/index.js:21:17)
[ Server!] at SecurePassword.verify (/home/jparker/my_apps/sass_one/advertiser/app/node_modules/secure-password/index.js:126:3)
[ Server!] at /home/jparker/my_apps/sass_one/advertiser/app/node_modules/secure-password/index.js:109:12
[ Server!] at new Promise (<anonymous>)
[ Server!] at SecurePassword.verify (/home/jparker/my_apps/sass_one/advertiser/app/node_modules/secure-password/index.js:108:12)
[ Server!] at login (/home/jparker/my_apps/sass_one/advertiser/app/src/apis.ts:34:48)
[ Server!] at <anonymous> (/home/jparker/my_apps/sass_one/advertiser/app/.wasp/out/sdk/wasp/server/utils.ts:26:5)
kapa.ai
kapa.ai6mo ago
Let me check the knowledge base for you. One moment! :books: ---------------------- The error message you're seeing is coming from the secure-password library. It's complaining that the hashBuf (the hashed password) is not the expected length of HASH_BYTES (128 bytes). This could happen if the hashed password stored in the database is not the correct length, or if it's not correctly encoded. The secure-password library expects the hashed password to be a Buffer of 128 bytes. When you're calling secure-password's verify method, make sure that the hashed password you're passing in is a Buffer of 128 bytes. If the hashed password is stored as a base64 string in the database, you'll need to convert it to a Buffer before passing it to verify. Here's how you might do this:
const passwordValidationResult = pwd.verify(Buffer.from(password), Buffer.from((providerData as EmailProviderData).hashedPassword, 'base64'));
const passwordValidationResult = pwd.verify(Buffer.from(password), Buffer.from((providerData as EmailProviderData).hashedPassword, 'base64'));
In this example, Buffer.from((providerData as EmailProviderData).hashedPassword, 'base64') converts the base64-encoded hashed password to a Buffer. If you're still having issues, you might want to check the value of the hashed password stored in the database and ensure that it's a base64-encoded string of the correct length. Sources: - Auth Overview (https://wasp-lang.dev/docs/auth/overview) - Username & Password Authentication (https://wasp-lang.dev/docs/auth/username-and-pass) - Migration from 0.11.X to 0.12.X (https://wasp-lang.dev/docs/migrate-from-0-11-to-0-12#username--password) ---------------------- Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: ---------------------- Sorry @JLegendz, I seem to be having a bit of trouble. Don't worry though, I've pinged my support and they will try to fix me up! :technologist: Perhaps try again in 15mins?
JLegendz
JLegendzOP6mo ago
@kapa.ai I was able to successfully login using the following:
export const login = async (req: Request, res: Response) => {
const { email, password } = req.body;

const providerId = createProviderId('email', email);
const authIdentity = await findAuthIdentity(providerId);

if (!authIdentity) {
throw new HttpError(400, 'Unknown user');
}

const providerData = deserializeAndSanitizeProviderData(authIdentity.providerData) as EmailProviderData;

// Validate the password using argon2
try {
const passwordMatch = await argon2.verify(providerData.hashedPassword, password);
if (!passwordMatch) {
throw new HttpError(400, 'Incorrect password');
}
} catch (error) {
throw new HttpError(500, 'Error verifying password');
}



// Password is correct, proceed with authentication
res.json({ message: 'Login successful' });
};
export const login = async (req: Request, res: Response) => {
const { email, password } = req.body;

const providerId = createProviderId('email', email);
const authIdentity = await findAuthIdentity(providerId);

if (!authIdentity) {
throw new HttpError(400, 'Unknown user');
}

const providerData = deserializeAndSanitizeProviderData(authIdentity.providerData) as EmailProviderData;

// Validate the password using argon2
try {
const passwordMatch = await argon2.verify(providerData.hashedPassword, password);
if (!passwordMatch) {
throw new HttpError(400, 'Incorrect password');
}
} catch (error) {
throw new HttpError(500, 'Error verifying password');
}



// Password is correct, proceed with authentication
res.json({ message: 'Login successful' });
};
But now I need to return the appropriate jwt, how should that be done in WASP?
kapa.ai
kapa.ai6mo ago
Let me check the knowledge base for you. One moment! :books: ---------------------- To return a JWT (JSON Web Token) in your login function, you can use the jsonwebtoken library. Here's how you might modify your login function to generate and return a JWT:
import jwt from 'jsonwebtoken';

export const login = async (req: Request, res: Response) => {
const { email, password } = req.body;

const providerId = createProviderId('email', email);
const authIdentity = await findAuthIdentity(providerId);

if (!authIdentity) {
throw new HttpError(400, 'Unknown user');
}

const providerData = deserializeAndSanitizeProviderData(authIdentity.providerData) as EmailProviderData;

// Validate the password using argon2
try {
const passwordMatch = await argon2.verify(providerData.hashedPassword, password);
if (!passwordMatch) {
throw new HttpError(400, 'Incorrect password');
}
} catch (error) {
throw new HttpError(500, 'Error verifying password');
}

// Password is correct, proceed with authentication

// User payload for JWT
const userPayload = { email, userId: authIdentity.userId };

// Retrieve the secret key from environment variables
const secretKey = process.env.JWT_SECRET_KEY;

if (!secretKey) {
throw new Error('JWT secret key is missing');
}

// Token options
const options = { expiresIn: '1h' }; // Token expires in 1 hour

// Generate the JWT
const token = jwt.sign(userPayload, secretKey, options);

// Send the JWT back to the client
res.json({ token });
};
import jwt from 'jsonwebtoken';

export const login = async (req: Request, res: Response) => {
const { email, password } = req.body;

const providerId = createProviderId('email', email);
const authIdentity = await findAuthIdentity(providerId);

if (!authIdentity) {
throw new HttpError(400, 'Unknown user');
}

const providerData = deserializeAndSanitizeProviderData(authIdentity.providerData) as EmailProviderData;

// Validate the password using argon2
try {
const passwordMatch = await argon2.verify(providerData.hashedPassword, password);
if (!passwordMatch) {
throw new HttpError(400, 'Incorrect password');
}
} catch (error) {
throw new HttpError(500, 'Error verifying password');
}

// Password is correct, proceed with authentication

// User payload for JWT
const userPayload = { email, userId: authIdentity.userId };

// Retrieve the secret key from environment variables
const secretKey = process.env.JWT_SECRET_KEY;

if (!secretKey) {
throw new Error('JWT secret key is missing');
}

// Token options
const options = { expiresIn: '1h' }; // Token expires in 1 hour

// Generate the JWT
const token = jwt.sign(userPayload, secretKey, options);

// Send the JWT back to the client
res.json({ token });
};
In this example, jwt.sign() is used to generate a JWT. The JWT contains the user's email and user ID, and is signed with a secret key retrieved from the environment variables. The JWT is then sent back to the client in the response. Please note that you need to have the jsonwebtoken library installed in your project and the JWT_SECRET_KEY environment variable set in your server environment. Sources: - Best Practices for Implementing Auth System in Chrome Extension Connected to OpenSaaS (https://discord.com/channels/686873244791210014/1223328118278983680) - JWT secret must not be hardcoded in the back-end code (https://github.com/wasp-lang/wasp/issues/129) - Randomly generate JWT secret at appropriate times? (https://github.com/wasp-lang/wasp/issues/174) ---------------------- Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: ---------------------- Sorry @JLegendz, I seem to be having a bit of trouble. Don't worry though, I've pinged my support and they will try to fix me up! :technologist: Perhaps try again in 15mins?
martinsos
martinsos6mo ago
@JLegendz I see you had a quite big conversation with Kapa, but I am curious, why would you want to use the auth methods via the API, what is the use case for that? If you want to use this to enable your users to access the app programmatically, then a better and more typical solution is generating special tokens for them (JWT) that they will use just for such purposes. Those are often called API tokens or something similar, and are a better option security-wise, as you can easily also implement revoking of them and similar, while if somebody gets your username and pass you are in trouble.
JLegendz
JLegendzOP6mo ago
Hi @martinsos , thank you so much for your reply, and I hope my use of Kapa isn’t a problem. It was very helpful although as you say, I might be going down the wrong path. Honestly im a bit ignorant to the best approach at this, but I’m building a mobile version of my app as well, and I want to allow mobile users to log into the wasp app, which seems to mean I need to create a login api. That’s what all this is about. I was passing a username and password, with the intention of then generating a jwt in wasp and sending that, and a refresh token back to the mobile user. All the back and forth is because when trying to match the passed in password to the hashed password with bcrypt, it never matched. I eventually realized that bcrypt was the wrong tool to test the hash with. My goal when I get back to my computer this evening is to try and generate that jwt with wasp. Is that process documented? I didn’t see it last I checked.
martinsos
martinsos6mo ago
@JLegendz sorry for being a bit late on answering this! That makes perfect sense, and I am afraid Wasp does fall a bit short on that front currently. So your effort is quite reasonable, but it is not so easy to implement it of course, and instead of you having to figure how to do it and implementing it on your own, it would be much better if we offered already read libraries / docs for this. We will certainly be immplementing this, but we don't have it yet. We have an issue for it here: https://github.com/wasp-lang/wasp/issues/1973 . Yeah, so in that case jwts are probably not the solution, but indeed the solution is in the direction you described. What are you building your mobile app with? Is it react native? What you will probably want to do is take a look at the code that Wasp generates in .wasp/out/web-app/src/auth, which will lead you to .wasp/out/sdk/wasp/auth/... and show you how this is currently implemented in Wasp for your frontend. Replicating that logic on your mobile app would give you what you need. But it is quite involved, much more involved than we would want it to be.
Want results from more Discord servers?
Add your server