Can't get @deno/kv-oauth to work
Hello, I am trying to use @deno/kv-oauth to enable login only for myself. I am using Angular in the frontend and Deno Deploy here. I would like to use Github OAuth for this and have already created an OAuth app—that should be fine. However, in my Angular frontend, I always get the following response:
https://secret.deno.dev/protected-route 401 UnauthorizedI haven't been able to get the whole thing to run locally yet, which makes it even more difficult. I would first show my Deno code here and then what I have in Angular. Maybe someone has an idea.
1 Reply
import { createGitHubOAuthConfig, createHelpers } from 'jsr:@deno/kv-oauth';
import { STATUS_CODE, StatusCode } from 'jsr:@std/http/status';
import { oauthCallback } from './handler/oauth-callback.ts';
import { getSession } from './utils/db.ts';
export const ALLOWED_USER = Deno.env.get('ALLOWED_GITHUB_USER');
const OAUTH_REDIRECT_URI = Deno.env.get('OAUTH_REDIRECT_URI');
if (!ALLOWED_USER) {
console.error('🚨 ALLOWED_GITHUB_USER not set!');
}
if (!OAUTH_REDIRECT_URI) {
console.error('🚨 OAUTH_REDIRECT_URI not set!');
}
export const oauthConfig = createGitHubOAuthConfig({
redirectUri: OAUTH_REDIRECT_URI,
});
export const {
signIn,
getSessionId,
signOut,
handleCallback,
} = createHelpers(oauthConfig);
const withCorsHeaders = (
message: string,
status: StatusCode = 200,
): Response => {
const body = JSON.stringify({ message });
const headers = new Headers();
headers.set('Content-Type', 'application/json');
headers.set('Access-Control-Allow-Origin', 'https://telesto.github.io');
headers.set('Access-Control-Allow-Credentials', 'true');
headers.set('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
headers.set('Access-Control-Allow-Headers', 'Content-Type');
return new Response(body, {
status,
headers,
});
};
import { createGitHubOAuthConfig, createHelpers } from 'jsr:@deno/kv-oauth';
import { STATUS_CODE, StatusCode } from 'jsr:@std/http/status';
import { oauthCallback } from './handler/oauth-callback.ts';
import { getSession } from './utils/db.ts';
export const ALLOWED_USER = Deno.env.get('ALLOWED_GITHUB_USER');
const OAUTH_REDIRECT_URI = Deno.env.get('OAUTH_REDIRECT_URI');
if (!ALLOWED_USER) {
console.error('🚨 ALLOWED_GITHUB_USER not set!');
}
if (!OAUTH_REDIRECT_URI) {
console.error('🚨 OAUTH_REDIRECT_URI not set!');
}
export const oauthConfig = createGitHubOAuthConfig({
redirectUri: OAUTH_REDIRECT_URI,
});
export const {
signIn,
getSessionId,
signOut,
handleCallback,
} = createHelpers(oauthConfig);
const withCorsHeaders = (
message: string,
status: StatusCode = 200,
): Response => {
const body = JSON.stringify({ message });
const headers = new Headers();
headers.set('Content-Type', 'application/json');
headers.set('Access-Control-Allow-Origin', 'https://telesto.github.io');
headers.set('Access-Control-Allow-Credentials', 'true');
headers.set('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
headers.set('Access-Control-Allow-Headers', 'Content-Type');
return new Response(body, {
status,
headers,
});
};
const handler = async (request: Request): Promise<Response> => {
if (request.method === 'OPTIONS') {
return new Response(null, {
headers: {
'Access-Control-Allow-Origin': 'https://tonyspegel.github.io',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
'Access-Control-Allow-Credentials': 'true',
},
});
}
const { pathname } = new URL(request.url);
switch (pathname) {
case '/oauth/signin':
return await signIn(request);
case '/oauth/callback':
return await oauthCallback(request);
case '/oauth/signout':
return await signOut(request);
case '/protected-route': {
const sessionId = await getSessionId(request);
if (!sessionId) {
return withCorsHeaders(
'Unauthenticated request',
STATUS_CODE.Unauthorized,
);
}
const user = await getSession(sessionId);
if (!user) {
return withCorsHeaders(
'No valid session found',
STATUS_CODE.Unauthorized,
);
}
return withCorsHeaders(
`Authenticated as ${user.login}`,
STATUS_CODE.OK,
);
}
default:
return withCorsHeaders('Route not found', STATUS_CODE.NotFound);
}
};
Deno.serve(handler);
const handler = async (request: Request): Promise<Response> => {
if (request.method === 'OPTIONS') {
return new Response(null, {
headers: {
'Access-Control-Allow-Origin': 'https://tonyspegel.github.io',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
'Access-Control-Allow-Credentials': 'true',
},
});
}
const { pathname } = new URL(request.url);
switch (pathname) {
case '/oauth/signin':
return await signIn(request);
case '/oauth/callback':
return await oauthCallback(request);
case '/oauth/signout':
return await signOut(request);
case '/protected-route': {
const sessionId = await getSessionId(request);
if (!sessionId) {
return withCorsHeaders(
'Unauthenticated request',
STATUS_CODE.Unauthorized,
);
}
const user = await getSession(sessionId);
if (!user) {
return withCorsHeaders(
'No valid session found',
STATUS_CODE.Unauthorized,
);
}
return withCorsHeaders(
`Authenticated as ${user.login}`,
STATUS_CODE.OK,
);
}
default:
return withCorsHeaders('Route not found', STATUS_CODE.NotFound);
}
};
Deno.serve(handler);
import { STATUS_CODE } from 'jsr:@std/http@0.221/status';
import { ALLOWED_USER, handleCallback } from '../main.ts';
import { setSession } from '../utils/db.ts';
export const oauthCallback = async (request: Request): Promise<Response> => {
const { sessionId, tokens } = await handleCallback(request);
const githubUserRes = await fetch('https://api.github.com/user', {
headers: {
Authorization: `bearer ${tokens.accessToken}`,
'User-Agent': 'scan-lab-login',
},
});
const githubUser = await githubUserRes.json();
if (githubUser.login !== ALLOWED_USER) {
return new Response('Unauthorized', {
status: STATUS_CODE.Unauthorized,
});
}
await setSession(sessionId, { login: githubUser.login });
return Response.redirect(
'https://telesto.github.io/sla-admin',
STATUS_CODE.TemporaryRedirect,
);
};
import { STATUS_CODE } from 'jsr:@std/http@0.221/status';
import { ALLOWED_USER, handleCallback } from '../main.ts';
import { setSession } from '../utils/db.ts';
export const oauthCallback = async (request: Request): Promise<Response> => {
const { sessionId, tokens } = await handleCallback(request);
const githubUserRes = await fetch('https://api.github.com/user', {
headers: {
Authorization: `bearer ${tokens.accessToken}`,
'User-Agent': 'scan-lab-login',
},
});
const githubUser = await githubUserRes.json();
if (githubUser.login !== ALLOWED_USER) {
return new Response('Unauthorized', {
status: STATUS_CODE.Unauthorized,
});
}
await setSession(sessionId, { login: githubUser.login });
return Response.redirect(
'https://telesto.github.io/sla-admin',
STATUS_CODE.TemporaryRedirect,
);
};
export const kv = await Deno.openKv();
export interface SessionUser {
login: string;
}
export const setSession = async (
sessionId: string,
user: SessionUser,
): Promise<void> => {
await kv.set(['oauth_user', sessionId], user);
};
export const getSession = async (
sessionId: string,
): Promise<SessionUser | null> => {
const entry = await kv.get<SessionUser>(['oauth_user', sessionId]);
return entry.value ?? null;
};
export const deleteSession = async (sessionId: string): Promise<void> => {
await kv.delete(['oauth_user', sessionId]);
};
export const kv = await Deno.openKv();
export interface SessionUser {
login: string;
}
export const setSession = async (
sessionId: string,
user: SessionUser,
): Promise<void> => {
await kv.set(['oauth_user', sessionId], user);
};
export const getSession = async (
sessionId: string,
): Promise<SessionUser | null> => {
const entry = await kv.get<SessionUser>(['oauth_user', sessionId]);
return entry.value ?? null;
};
export const deleteSession = async (sessionId: string): Promise<void> => {
await kv.delete(['oauth_user', sessionId]);
};
@Injectable({ providedIn: 'root' })
export class AuthService {
private readonly backendUrl = environment.loginUrl;
private readonly http = inject(HttpClient);
private readonly isAuthenticated = signal<boolean>(false);
checkAuth() {
return this.http.get(`${this.backendUrl}/protected-route`, { withCredentials: true }).pipe(
tap(() => this.isAuthenticated.set(true)),
map(() => true),
catchError(() => {
this.isAuthenticated.set(false);
return of(false);
})
);
}
login() {
window.location.href = `${this.backendUrl}/oauth/signin`;
}
logout() {
this.isAuthenticated.set(false);
window.location.href = `${this.backendUrl}/oauth/signout`;
}
}
@Injectable({ providedIn: 'root' })
export class AuthService {
private readonly backendUrl = environment.loginUrl;
private readonly http = inject(HttpClient);
private readonly isAuthenticated = signal<boolean>(false);
checkAuth() {
return this.http.get(`${this.backendUrl}/protected-route`, { withCredentials: true }).pipe(
tap(() => this.isAuthenticated.set(true)),
map(() => true),
catchError(() => {
this.isAuthenticated.set(false);
return of(false);
})
);
}
login() {
window.location.href = `${this.backendUrl}/oauth/signin`;
}
logout() {
this.isAuthenticated.set(false);
window.location.href = `${this.backendUrl}/oauth/signout`;
}
}
export const routes: Routes = [
{ path: 'login', component: LoginComponent },
{ path: 'orders', component: OrdersComponent, canActivate: [authGuard] },
{
path: 'orders',
children: [
{ path: '', component: OrdersComponent },
{ path: ':id', component: OrderComponent },
],
},
{ path: '**', redirectTo: 'orders' },
];
export const routes: Routes = [
{ path: 'login', component: LoginComponent },
{ path: 'orders', component: OrdersComponent, canActivate: [authGuard] },
{
path: 'orders',
children: [
{ path: '', component: OrdersComponent },
{ path: ':id', component: OrderComponent },
],
},
{ path: '**', redirectTo: 'orders' },
];
export const authGuard: CanActivateFn = () => {
const authService = inject(AuthService);
const router = inject(Router);
return authService.checkAuth().pipe(
map(isAuth => {
if (!isAuth) {
router.navigate(['/login']);
return false;
}
return true;
})
);
};
export const authGuard: CanActivateFn = () => {
const authService = inject(AuthService);
const router = inject(Router);
return authService.checkAuth().pipe(
map(isAuth => {
if (!isAuth) {
router.navigate(['/login']);
return false;
}
return true;
})
);
};