feat(fxa-auth-server) set display name from 3rd party auth data

This commit is contained in:
Mill 2022-05-25 09:03:41 -07:00
Родитель bc02c80694
Коммит 123a585850
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 191635F49DE50466
7 изменённых файлов: 137 добавлений и 5 удалений

Просмотреть файл

@ -13,6 +13,8 @@ const PATH_PREFIX = '/v1';
// here other than that it didn't fail in error
const DeleteCacheResponse = isA.any();
const UpdateDisplayNameResponse = isA.any();
module.exports = function (log, config, statsd) {
const ProfileAPI = createBackendServiceAPI(
log,
@ -29,6 +31,19 @@ module.exports = function (log, config, statsd) {
response: DeleteCacheResponse,
},
},
updateDisplayName: {
path: `${PATH_PREFIX}/_display_name/:uid`,
method: 'POST',
validate: {
params: {
uid: isA.string().required(),
},
payload: {
name: isA.string().required(),
},
response: UpdateDisplayNameResponse,
}
},
},
statsd
);
@ -49,5 +64,13 @@ module.exports = function (log, config, statsd) {
throw err;
}
},
async updateDisplayName(uid, name) {
try {
return await api.updateDisplayName(uid, { name: name });
} catch (err) {
log.error('profile.updateDisplayName.failed', { uid, name, err});
throw err;
}
},
};
};

Просмотреть файл

@ -172,7 +172,7 @@ module.exports = function (
const util = require('./util')(log, config, config.smtp.redirectDomain);
const { linkedAccountRoutes } = require('./linked-accounts');
const linkedAccounts = linkedAccountRoutes(log, db, config, mailer);
const linkedAccounts = linkedAccountRoutes(log, db, config, mailer, profile);
let basePath = url.parse(config.publicUrl).path;
if (basePath === '/') {

Просмотреть файл

@ -1,7 +1,7 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import { AuthLogger, AuthRequest } from '../types';
import { AuthLogger, AuthRequest, ProfileClient } from '../types';
import { ConfigType } from '../../config';
import { OAuth2Client } from 'google-auth-library';
import axios from 'axios';
@ -32,7 +32,8 @@ export class LinkedAccountHandler {
private log: AuthLogger,
private db: any,
private config: ConfigType,
private mailer: any
private mailer: any,
private profile: ProfileClient,
) {
const tokenCodeConfig = config.signinConfirmation.tokenVerificationCode;
this.tokenCodeLifetime =
@ -166,6 +167,7 @@ export class LinkedAccountHandler {
const userid = idToken.sub;
const email = idToken.email;
const name = idToken.name;
let accountRecord;
let linkedAccountRecord = await this.db.getLinkedAccount(userid, provider);
@ -176,6 +178,10 @@ export class LinkedAccountHandler {
accountRecord = await this.db.accountRecord(email);
await this.db.createLinkedAccount(accountRecord.uid, userid, provider);
if (name) {
await this.profile.updateDisplayName(accountRecord.uid, name);
}
const geoData = request.app.geo;
const ip = request.app.clientAddress;
const { deviceId, flowId, flowBeginTime } = await request.app
@ -237,6 +243,10 @@ export class LinkedAccountHandler {
locale: request.app.acceptLanguage,
});
await this.db.createLinkedAccount(accountRecord.uid, userid, provider);
if (name) {
await this.profile.updateDisplayName(accountRecord.uid, name);
}
// Currently, we treat accounts created from a linked account as a new
// registration and emit the correspond event. Note that depending on
// where might not be a top of funnel for this completion event.
@ -300,9 +310,10 @@ export const linkedAccountRoutes = (
log: AuthLogger,
db: any,
config: ConfigType,
mailer: any
mailer: any,
profile: ProfileClient,
) => {
const handler = new LinkedAccountHandler(log, db, config, mailer);
const handler = new LinkedAccountHandler(log, db, config, mailer, profile);
return [
{

Просмотреть файл

@ -67,6 +67,7 @@ export interface AuthRequest extends Request {
export interface ProfileClient {
deleteCache(uid: string): Promise<void>;
updateDisplayName(uid: string, name: string): Promise<void>;
}
// Container token types

Просмотреть файл

@ -0,0 +1,56 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
const Joi = require('@hapi/joi');
const db = require('../../db');
const notifyProfileUpdated = require('../../updates-queue');
const EMPTY = Object.create(null);
// We're pretty liberal with what's allowed in a display-name,
// but we exclude the following classes of characters:
//
// \u0000-\u001F - C0 (ascii) control characters
// \u007F - ascii DEL character
// \u0080-\u009F - C1 (ansi escape) control characters
// \u2028-\u2029 - unicode line/paragraph separator
// \uE000-\uF8FF - BMP private use area
// \uFFF9-\uFFFC - unicode specials prior to the replacement character
// \uFFFE-\uFFFF - unicode this-is-not-a-character specials
//
// Note that the unicode replacement character \uFFFD is explicitly allowed,
// and clients may use it to replace other disallowed characters.
//
// We might tweak this list in future.
// eslint-disable-next-line no-control-regex
const ALLOWED_DISPLAY_NAME_CHARS = /^(?:[^\u0000-\u001F\u007F\u0080-\u009F\u2028-\u2029\uE000-\uF8FF\uFFF9-\uFFFC\uFFFE-\uFFFF])*$/;
module.exports = {
auth: {
strategy: 'secretBearerToken',
},
validate: {
payload: {
name: Joi.string()
.max(256)
.required()
.allow('')
.regex(ALLOWED_DISPLAY_NAME_CHARS),
},
params: {
uid: Joi.string(),
}
},
handler: async function displayNamePost(req) {
const uid = req.params.uid;
return req.server.methods.profileCache.drop(uid).then(() => {
const payload = req.payload;
return db.setDisplayName(uid, payload.name).then(() => {
notifyProfileUpdated(uid); // Don't wait on promise
return EMPTY;
});
});
},
};

Просмотреть файл

@ -84,6 +84,12 @@ module.exports = [
path: v('/display_name'),
config: require('./routes/display_name/post'),
},
// This is an internal-only route that allows us to set profile name from the auth server
{
method: 'POST',
path: v('/_display_name/{uid}'),
config: require('./routes/display_name/post-from-auth-server'),
},
{
method: 'DELETE',
path: v('/cache/{uid}'),

Просмотреть файл

@ -1330,6 +1330,19 @@ describe('api', function () {
describe('/display_name', function () {
var tok = token();
const EXPECTED_TOKEN = 'thisisnotthedefault';
let origSecretBearerToken = null;
before(function () {
origSecretBearerToken = config.get('secretBearerToken');
config.set('secretBearerToken', EXPECTED_TOKEN);
});
after(function () {
config.set('secretBearerToken', origSecretBearerToken);
});
describe('GET', function () {
it('should return a displayName', function () {
mock.token({
@ -1420,6 +1433,28 @@ describe('api', function () {
});
});
it('should post a new display name via secretBearerToken', function () {
var NAME = 'Spock';
return Server.api
.post({
url: '/_display_name/' + USERID,
payload: {
name: NAME,
},
headers: {
authorization: 'Bearer ' + EXPECTED_TOKEN,
},
})
.then(function (res) {
assert.equal(res.statusCode, 200);
assertSecurityHeaders(res);
return db.getDisplayName(USERID);
})
.then(function (res) {
assert.equal(res.displayName, NAME);
});
});
it('should fail post if display name longer than 256 chars', function () {
var NAME = Array.from('x'.repeat('257')).join('');
mock.token({