Which strategy to use: “3rd party” cookie #237
Replies: 4 comments 10 replies
-
|
Is this as simple as using the “FormStrategy” example, modified? |
Beta Was this translation helpful? Give feedback.
-
|
How do you do the login? Is the login form going to live inside Remix? Or will you send the user to the .NET app which will handle the login and set the cookie? |
Beta Was this translation helpful? Give feedback.
-
|
Once I understood how to use an external cookie in Remix - https://dev.to/2ezpz2plzme/authenticating-remix-cookie-sessions-with-django-cookie-sessions-36h9 - adopting remix-auth-form was trivial // session.server.ts
const SESSION_SECRET = "A_SUPER_SECRET_VALUE"
export const API_COOKIE_NAME = ".AspNetCore.Identity.Application";
export const sessionStorage = createCookieSessionStorage({
cookie: {
name: 'sessionid',
path: '/',
secrets: [SESSION_SECRET],
secure: process.env.NODE_ENV === 'production',
// More cookie options could be added here, but we will be using the cookie options from the fetch responses later.
},
})// auth.server.ts
export let authenticator = new Authenticator<string>(sessionStorage);
function login(username: string, password: string) {
return fetch("https://localhost:5001/auth/login", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
username,
password,
}),
});
}
authenticator.use(
new FormStrategy(async ({ form }) => {
let username = form.get("username");
let password = form.get("password");
invariant(typeof username === "string", "username must be a string");
invariant(username.length > 0, "username must not be empty");
invariant(typeof password === "string", "password must be a string");
invariant(password.length > 0, "password must not be empty");
let response = await login(username, password);
const setCookieHeader = response.headers.get("Set-Cookie");
if (!setCookieHeader) {
throw new Error('no Set-Cookie header')
}
const parsedResponseCookies = setCookie.parse(
setCookie.splitCookiesString(setCookieHeader)
);
const sessionIdCookie = parsedResponseCookies.find(
(cookie) => cookie.name === API_COOKIE_NAME
);
if (!sessionIdCookie) {
throw new Error(`no ${API_COOKIE_NAME} cookie found`);
}
// Store the response's `sessionid` cookie into the headers.
const { value } = sessionIdCookie;
return value;
}),
// each strategy has a name and can be changed to use another one
// same strategy multiple times, especially useful for the OAuth2 strategy.
"user-pass"
);// auth.server.ts
import { API_COOKIE_NAME } from "~/session.server";
import { authenticator } from "~/auth.server";
export async function fetchWithCookie(
request: Request,
input: RequestInfo | URL,
init?: RequestInit
): Promise<Response> {
let user = await authenticator.isAuthenticated(request, {
failureRedirect: "/login",
});
const headers = {
"Content-Type": "application/json",
Cookie: `${API_COOKIE_NAME}=${user}`,
...init?.headers,
};
return await fetch(input, { ...init, headers });
} |
Beta Was this translation helpful? Give feedback.
{{title}}
{{editor}}'s edit
{{editor}}'s edit
-
|
For completeness, my .NET API also supports Google, Facebook, etc login, which is a different flow. With this flow, there is no API response, because of the redirects involved, so here, when you get back to the frontend, there is no So I implemented another Strategy to handle that, which is very simple, and basically is a copy/paste of // remix-auth-oauth-callback.ts
import type { AppLoadContext, Request, SessionStorage } from "@remix-run/node";
import type { AuthenticateOptions } from "remix-auth";
import { Strategy } from "remix-auth";
interface OauthCallbackStrategyVerifyParams {
request: Request;
context?: AppLoadContext;
}
export class OauthCallbackStrategy<User> extends Strategy<
User,
OauthCallbackStrategyVerifyParams
> {
name = "oauth-callback";
async authenticate(
request: Request,
sessionStorage: SessionStorage,
options: AuthenticateOptions
): Promise<User> {
try {
let user = await this.verify({ request, context: options.context });
return this.success(user, request, sessionStorage, options);
} catch (error) {
if (error instanceof Error) {
return await this.failure(
error.message,
request,
sessionStorage,
options,
error
);
}
if (typeof error === "string") {
return await this.failure(
error,
request,
sessionStorage,
options,
new Error(error)
);
}
return await this.failure(
"Unknown error",
request,
sessionStorage,
options,
new Error(JSON.stringify(error, null, 2))
);
}
}
}//auth.server.ts
function getApiCookie(headers: Headers) {
let cookieHeader = headers.get("Set-Cookie");
if (!cookieHeader) {
cookieHeader = headers.get("Cookie");
}
if (!cookieHeader) {
throw new Error("no Cookie or Set-Cookie header");
}
const parsedResponseCookies = setCookie.parse(
setCookie.splitCookiesString(cookieHeader)
);
const apiCookie = parsedResponseCookies.find(
(cookie) => cookie.name === API_COOKIE_NAME
);
if (!apiCookie) {
throw new Error(`no ${API_COOKIE_NAME} cookie found`);
}
return apiCookie;
}
authenticator
.use(
new FormStrategy(async ({ form }) => {
let username = form.get("username");
let password = form.get("password");
invariant(
typeof username === "string",
"username must be a string"
);
invariant(username.length > 0, "username must not be empty");
invariant(
typeof password === "string",
"password must be a string"
);
invariant(password.length > 0, "password must not be empty");
let response = await login(username, password);
const apiCookie = getApiCookie(response.headers);
const { value } = apiCookie;
return value;
}),
"user-pass"
)
.use(
new OauthCallbackStrategy(async ({ request, context }) => {
const apiCookie = getApiCookie(request.headers);
const { value } = apiCookie;
return value;
}),
"oauth-callback"
);// routes/login.tsx
// snipped
{oAuthProviders.map((provider) => (
<form
key={provider}
action={`${apiBaseUrl}/auth/oauth?provider=${provider}&returnUrl=http://localhost:3000/oauth-callback`}
method="post"
>
<Button type="submit">Login with {provider}</Button>
</form>
))}// routes/oauth-callback
export async function loader({ request }: LoaderArgs) {
// If the user is already authenticated redirect to /dashboard directly
await authenticator.isAuthenticated(request, {
successRedirect: "/me",
});
return await authenticator.authenticate("oauth-callback", request, {
successRedirect: "/me",
failureRedirect: "/login",
});
}Reading this again, I should probably change that strategy name, because really it is probably rather a There is probably a more elegant way to do this, but we wont be moving away from our API + Auth server, and OAuth works like it works, but happy to hear suggestions |
Beta Was this translation helpful? Give feedback.


{{title}}
{{editor}}'s edit
{{editor}}'s edit
-
I have an existing .NET API that handles authentication. Given a valid username and password, it produces an HttpOnly cookie.
This cookie is used to then authenticate subsequent API requests.
I want to continue to use this API from my Remix loader
Which strategy would I use? Or is this project not applicable to me?
I’m basically looking for a simple way to know the user is not authenticated (response 401 from a “get user details” endpoint), and redirect to login. This library seems to have nice hooks for that purpose.
Beta Was this translation helpful? Give feedback.
All reactions