Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement NPM web login #4517

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/api/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import bodyParser from 'body-parser';
import express, { Router } from 'express';

import { Auth } from '@verdaccio/auth';
Expand All @@ -18,6 +19,7 @@ import publish from './publish';
import search from './search';
import stars from './stars';
import user from './user';
import login from './v1/login';
import profile from './v1/profile';
import v1Search from './v1/search';
import token from './v1/token';
Expand All @@ -44,6 +46,8 @@ export default function (config: Config, auth: Auth, storage: Storage): Router {
app.use(auth.apiJWTmiddleware());
app.use(express.json({ strict: false, limit: config.max_body_size || '10mb' }));
app.use(antiLoop(config));
// TODO : to be removed once we have a react login route
app.use(bodyParser.urlencoded({ extended: true }));
// encode / in a scoped package name to be matched as a single parameter in routes
app.use(encodeScopePackage);
// for "npm whoami"
Expand All @@ -54,6 +58,7 @@ export default function (config: Config, auth: Auth, storage: Storage): Router {
distTags(app, auth, storage);
publish(app, auth, storage);
ping(app);
login(app, auth, storage, config);
stars(app, storage);
v1Search(app, auth, storage);
token(app, auth, storage, config);
Expand Down
112 changes: 112 additions & 0 deletions packages/api/src/v1/login.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { randomUUID } from 'crypto';
import { Request, Response, Router } from 'express';
import { isUUID } from 'validator';

import { Auth, getApiToken } from '@verdaccio/auth';
import { createRemoteUser } from '@verdaccio/config';
import { HEADERS, HTTP_STATUS, errorUtils } from '@verdaccio/core';
import { Storage } from '@verdaccio/store';
import { Config, RemoteUser } from '@verdaccio/types';
import { getAuthenticatedMessage } from '@verdaccio/utils';

import { $NextFunctionVer } from '../../types/custom';

function addNpmLoginApi(route: Router, auth: Auth, storage: Storage, config: Config): void {
route.post('/-/v1/login', function (req: Request, res: Response): void {
const sessionId = randomUUID();
res.status(200).json({
loginUrl: 'http://localhost:8000/-/v1/login/cli/' + sessionId,
doneUrl: 'http://localhost:8000/-/v1/done?sessionId=' + sessionId,
});
});
route.get('/-/v1/done', function (req: Request, res: Response): void {
if (!req.query.sessionId) {
res.status(400).json({ error: 'missing session id' });
return;
}

const sessionId = req.query.sessionId.toString();
if (!isUUID(sessionId, 4)) {
res.status(400).json({ error: 'invalid session id' });
return;
}
// const tokens = storage.readTokens();
// TODO : check if the token have been created in storage with the sessionId as key
const ready = false;

if (!ready) {
// TODO : variable retry-after should be configurable in the config
res.header('retry-after', '5');
res.status(202).json({});
return;
}
res.status(200).json({ token: 'sample_token_not working' });
});

route.get('/-/v1/login/cli/:sessionId', function (req: Request, res: Response): void {
// TODO : This should be a webUI route but i dunno how to do it with React
res.send(`
<form action="/-/v1/login/cli/${req.params.sessionId}" method="post">
<label for="username">Username:</label>
<input type="text" id="username" name="username" required><br><br>
<label for="password">Password:</label>
<input type="password" id="password" name="password" required><br><br>
<input type="submit" value="Login">
</form>
`);
Comment on lines +48 to +56

Check failure

Code scanning / CodeQL

Reflected cross-site scripting High

Cross-site scripting vulnerability due to a
user-provided value
.
});

route.post(
'/-/v1/login/cli/:sessionId',
async function (req: Request, res: Response, next: $NextFunctionVer): Promise<void> {
const { username, password } = req.body;

if (!req.params.sessionId) {
res.status(400).json({ error: 'missing session id' });
return;
}

const sessionId = req.params.sessionId.toString();
if (!isUUID(sessionId, 4)) {
res.status(400).json({ error: 'invalid session id' });
return;
}

// Perform authentication logic here
auth.authenticate(
username,
password,
async function callbackAuthenticate(err, user): Promise<void> {
if (err) {
return next(errorUtils.getCode(HTTP_STATUS.UNAUTHORIZED, err.message));
}

const restoredRemoteUser: RemoteUser = createRemoteUser(username, user?.groups || []);
const token = await getApiToken(auth, config, restoredRemoteUser, password);

if (!token) {
return next(errorUtils.getUnauthorized());
}

res.status(HTTP_STATUS.CREATED);
res.set(HEADERS.CACHE_CONTROL, 'no-cache, no-store');

const message = getAuthenticatedMessage(restoredRemoteUser.name ?? '');
// TODO : save the token in storage with the sessionId as key
await storage.saveToken({
user: restoredRemoteUser.name as string,
token: token,
key: sessionId,
readonly: false,
created: '',
});
return next({
ok: message,
});
}
);
}
);
}

export default addNpmLoginApi;