Implement Webauthn 🎉 (#5088)
* Implement Webauthn 🎉 * Share hexifyAB * Move hr inside template and add AttestationChallenges janitor daemon * Apply suggestions from code review Co-Authored-By: Acid Chicken (硫酸鶏) <root@acid-chicken.com> * Add newline at the end of file * Fix stray newline in promise chain * Ignore var in try{}catch(){} block Co-Authored-By: Acid Chicken (硫酸鶏) <root@acid-chicken.com> * Add missing comma * Add missing semicolon * Support more attestation formats * add support for more key types and linter pass * Refactor * Refactor * credentialId --> id * Fix * Improve readability * Add indexes * fixes for credentialId->id * Avoid changing store state * Fix syntax error and code style * Remove unused import * Refactor of getkey API * Create 1561706992953-webauthn.ts * Update ja-JP.yml * Add type annotations * Fix code style * Specify depedency version * Fix code style * Fix janitor daemon and login requesting 2FA regardless of status
This commit is contained in:
46
src/models/entities/attestation-challenge.ts
Normal file
46
src/models/entities/attestation-challenge.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import { PrimaryColumn, Entity, JoinColumn, Column, ManyToOne, Index } from 'typeorm';
|
||||
import { User } from './user';
|
||||
import { id } from '../id';
|
||||
|
||||
@Entity()
|
||||
export class AttestationChallenge {
|
||||
@PrimaryColumn(id())
|
||||
public id: string;
|
||||
|
||||
@Index()
|
||||
@PrimaryColumn(id())
|
||||
public userId: User['id'];
|
||||
|
||||
@ManyToOne(type => User, {
|
||||
onDelete: 'CASCADE'
|
||||
})
|
||||
@JoinColumn()
|
||||
public user: User | null;
|
||||
|
||||
@Index()
|
||||
@Column('varchar', {
|
||||
length: 64,
|
||||
comment: 'Hex-encoded sha256 hash of the challenge.'
|
||||
})
|
||||
public challenge: string;
|
||||
|
||||
@Column('timestamp with time zone', {
|
||||
comment: 'The date challenge was created for expiry purposes.'
|
||||
})
|
||||
public createdAt: Date;
|
||||
|
||||
@Column('boolean', {
|
||||
comment:
|
||||
'Indicates that the challenge is only for registration purposes if true to prevent the challenge for being used as authentication.',
|
||||
default: false
|
||||
})
|
||||
public registrationChallenge: boolean;
|
||||
|
||||
constructor(data: Partial<AttestationChallenge>) {
|
||||
if (data == null) return;
|
||||
|
||||
for (const [k, v] of Object.entries(data)) {
|
||||
(this as any)[k] = v;
|
||||
}
|
||||
}
|
||||
}
|
@ -76,6 +76,11 @@ export class UserProfile {
|
||||
})
|
||||
public twoFactorEnabled: boolean;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
public securityKeysAvailable: boolean;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 128, nullable: true,
|
||||
comment: 'The password hash of the User. It will be null if the origin of the user is local.'
|
||||
|
48
src/models/entities/user-security-key.ts
Normal file
48
src/models/entities/user-security-key.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import { PrimaryColumn, Entity, JoinColumn, Column, ManyToOne, Index } from 'typeorm';
|
||||
import { User } from './user';
|
||||
import { id } from '../id';
|
||||
|
||||
@Entity()
|
||||
export class UserSecurityKey {
|
||||
@PrimaryColumn('varchar', {
|
||||
comment: 'Variable-length id given to navigator.credentials.get()'
|
||||
})
|
||||
public id: string;
|
||||
|
||||
@Index()
|
||||
@Column(id())
|
||||
public userId: User['id'];
|
||||
|
||||
@ManyToOne(type => User, {
|
||||
onDelete: 'CASCADE'
|
||||
})
|
||||
@JoinColumn()
|
||||
public user: User | null;
|
||||
|
||||
@Index()
|
||||
@Column('varchar', {
|
||||
comment:
|
||||
'Variable-length public key used to verify attestations (hex-encoded).'
|
||||
})
|
||||
public publicKey: string;
|
||||
|
||||
@Column('timestamp with time zone', {
|
||||
comment:
|
||||
'The date of the last time the UserSecurityKey was successfully validated.'
|
||||
})
|
||||
public lastUsed: Date;
|
||||
|
||||
@Column('varchar', {
|
||||
comment: 'User-defined name for this key',
|
||||
length: 30
|
||||
})
|
||||
public name: string;
|
||||
|
||||
constructor(data: Partial<UserSecurityKey>) {
|
||||
if (data == null) return;
|
||||
|
||||
for (const [k, v] of Object.entries(data)) {
|
||||
(this as any)[k] = v;
|
||||
}
|
||||
}
|
||||
}
|
@ -37,6 +37,8 @@ import { FollowingRepository } from './repositories/following';
|
||||
import { AbuseUserReportRepository } from './repositories/abuse-user-report';
|
||||
import { AuthSessionRepository } from './repositories/auth-session';
|
||||
import { UserProfile } from './entities/user-profile';
|
||||
import { AttestationChallenge } from './entities/attestation-challenge';
|
||||
import { UserSecurityKey } from './entities/user-security-key';
|
||||
import { HashtagRepository } from './repositories/hashtag';
|
||||
import { PageRepository } from './repositories/page';
|
||||
import { PageLikeRepository } from './repositories/page-like';
|
||||
@ -52,6 +54,8 @@ export const PollVotes = getRepository(PollVote);
|
||||
export const Users = getCustomRepository(UserRepository);
|
||||
export const UserProfiles = getRepository(UserProfile);
|
||||
export const UserKeypairs = getRepository(UserKeypair);
|
||||
export const AttestationChallenges = getRepository(AttestationChallenge);
|
||||
export const UserSecurityKeys = getRepository(UserSecurityKey);
|
||||
export const UserPublickeys = getRepository(UserPublickey);
|
||||
export const UserLists = getCustomRepository(UserListRepository);
|
||||
export const UserListJoinings = getRepository(UserListJoining);
|
||||
|
@ -1,7 +1,7 @@
|
||||
import $ from 'cafy';
|
||||
import { EntityRepository, Repository, In } from 'typeorm';
|
||||
import { User, ILocalUser, IRemoteUser } from '../entities/user';
|
||||
import { Emojis, Notes, NoteUnreads, FollowRequests, Notifications, MessagingMessages, UserNotePinings, Followings, Blockings, Mutings, UserProfiles, UserGroupJoinings } from '..';
|
||||
import { Emojis, Notes, NoteUnreads, FollowRequests, Notifications, MessagingMessages, UserNotePinings, Followings, Blockings, Mutings, UserProfiles, UserSecurityKeys, UserGroupJoinings } from '..';
|
||||
import { ensure } from '../../prelude/ensure';
|
||||
import config from '../../config';
|
||||
import { SchemaType } from '../../misc/schema';
|
||||
@ -156,6 +156,11 @@ export class UserRepository extends Repository<User> {
|
||||
detail: true
|
||||
}),
|
||||
twoFactorEnabled: profile!.twoFactorEnabled,
|
||||
securityKeys: profile!.twoFactorEnabled
|
||||
? UserSecurityKeys.count({
|
||||
userId: user.id
|
||||
}).then(result => result >= 1)
|
||||
: false,
|
||||
twitter: profile!.twitter ? {
|
||||
id: profile!.twitterUserId,
|
||||
screenName: profile!.twitterScreenName
|
||||
@ -195,6 +200,15 @@ export class UserRepository extends Repository<User> {
|
||||
clientData: profile!.clientData,
|
||||
email: profile!.email,
|
||||
emailVerified: profile!.emailVerified,
|
||||
securityKeysList: profile!.twoFactorEnabled
|
||||
? UserSecurityKeys.find({
|
||||
where: {
|
||||
userId: user.id
|
||||
},
|
||||
select: ['id', 'name', 'lastUsed']
|
||||
})
|
||||
: []
|
||||
|
||||
} : {}),
|
||||
|
||||
...(relation ? {
|
||||
|
Reference in New Issue
Block a user