-
Notifications
You must be signed in to change notification settings - Fork 1
Feat 21 Two Factor Authentication #89
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
base: dev
Are you sure you want to change the base?
Changes from all commits
eb985d3
6405190
ae71f89
4989c60
37e0575
913a345
eae28f3
6f3fb1d
1d22ae1
78af46a
b40121c
b97ea77
044f155
c392173
81bee8a
365eeea
e78c0a9
cf558f8
b13f8c2
cbc2100
cbac09c
fe4a8ce
5bc119e
a59d496
f764cc0
44457e3
cc7a293
7b84518
0a174be
e18fa9e
6cb13e7
b25b3fe
369b7f2
b7492a1
a2ade3f
5caa59a
3e2fcf9
5ffb851
e9a4469
020f54a
b83a23c
edb5f5c
204f398
90f424f
3457b45
ef09a32
2b3f859
231e73e
7a0c069
9e19c86
d0b73a6
7be6e37
32bc4cc
ab6794c
410371a
0058701
ffc9d19
3772ad1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| "use strict"; | ||
|
|
||
| const columns = [ | ||
| // Email OTP for 2FA | ||
| { | ||
| name: "twoFactorOtp", | ||
| type: "STRING", | ||
| allowNull: true, | ||
| defaultValue: null, | ||
| }, | ||
| { | ||
| name: "twoFactorOtpExpiresAt", | ||
| type: "DATE", | ||
| allowNull: true, | ||
| defaultValue: null, | ||
| }, | ||
| // LDAP support | ||
| { | ||
| name: "ldapUsername", | ||
| type: "STRING", | ||
| allowNull: true, | ||
| defaultValue: null, | ||
| }, | ||
| // ORCID support | ||
| { | ||
| name: "orcidId", | ||
| type: "STRING", | ||
| allowNull: true, | ||
| defaultValue: null, | ||
| unique: true, | ||
| }, | ||
| // SAML support | ||
| { | ||
| name: "samlNameId", | ||
| type: "STRING", | ||
| allowNull: true, | ||
| defaultValue: null, | ||
| unique: true, | ||
| }, | ||
| // Multi-method 2FA configuration | ||
| { | ||
| name: "twoFactorMethods", | ||
| type: "JSON", | ||
| allowNull: false, | ||
| defaultValue: [], | ||
| }, | ||
| { | ||
| name: "totpSecret", | ||
| type: "STRING", | ||
| allowNull: true, | ||
| defaultValue: null, | ||
| }, | ||
| ]; | ||
|
|
||
| /** @type {import('sequelize-cli').Migration} */ | ||
| module.exports = { | ||
| async up(queryInterface, Sequelize) { | ||
| await queryInterface.changeColumn("user", "email", { | ||
| type: Sequelize.STRING, | ||
| allowNull: true, | ||
| unique: true | ||
| }); | ||
|
|
||
| for (const column of columns) { | ||
| await queryInterface.addColumn("user", column.name, { | ||
| type: Sequelize[column.type], | ||
| defaultValue: column.defaultValue, | ||
| allowNull: column.allowNull, | ||
| ...(column.unique ? { unique: true } : {}), | ||
| }); | ||
| } | ||
| }, | ||
|
|
||
| async down(queryInterface, Sequelize) { | ||
| for (const column of columns) { | ||
| await queryInterface.removeColumn("user", column.name); | ||
| } | ||
| }, | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,154 @@ | ||
| "use strict"; | ||
|
|
||
| const settings = [ | ||
| { | ||
| key: "system.auth.orcid.enabled", | ||
| type: "boolean", | ||
| value: false, | ||
| description: "Enable ORCID login flow" | ||
| }, | ||
| { | ||
| key: "system.auth.ldap.enabled", | ||
| type: "boolean", | ||
| value: false, | ||
| description: "Enable LDAP login flow" | ||
| }, | ||
| { | ||
| key: "system.auth.saml.enabled", | ||
| type: "boolean", | ||
| value: false, | ||
| description: "Enable SAML login flow" | ||
| }, | ||
| { | ||
| key: "system.auth.local.2fa.required", | ||
| type: "boolean", | ||
| value: false, | ||
| description: "Require 2FA setup for local login users" | ||
| }, | ||
| { | ||
| key: "system.auth.orcid.2fa.required", | ||
| type: "boolean", | ||
| value: false, | ||
| description: "Require 2FA setup for ORCID login users" | ||
| }, | ||
| { | ||
| key: "system.auth.ldap.2fa.required", | ||
| type: "boolean", | ||
| value: false, | ||
| description: "Require 2FA setup for LDAP login users" | ||
| }, | ||
| { | ||
| key: "system.auth.saml.2fa.required", | ||
| type: "boolean", | ||
| value: false, | ||
| description: "Require 2FA setup for SAML login users" | ||
| }, | ||
| { | ||
| key: "system.auth.redirect.baseUrl", | ||
| type: "string", | ||
| value: "http://localhost:3000", | ||
| description: "Frontend base URL for authentication redirects (login success, OAuth callback, and 2FA verification pages)" | ||
| }, | ||
| { | ||
| key: "system.auth.orcid.clientId", | ||
| type: "string", | ||
| value: "", | ||
| description: "ORCID OAuth client id (changes require a restart of the server)", | ||
| onlyAdmin: true | ||
| }, | ||
| { | ||
| key: "system.auth.orcid.clientSecret", | ||
| type: "string", | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [QUESTION] i'm not very knowledgable about security, but is this (and a few others in this file) fine to put as a string in the database? |
||
| value: "", | ||
| description: "ORCID OAuth client secret (changes require a restart of the server)", | ||
| onlyAdmin: true | ||
| }, | ||
| { | ||
| key: "system.auth.orcid.callbackUrl", | ||
| type: "string", | ||
| value: "", | ||
| description: "ORCID callback URL (changes require a restart of the server)" | ||
| }, | ||
| { | ||
| key: "system.auth.orcid.sandbox", | ||
| type: "boolean", | ||
| value: true, | ||
| description: "Use ORCID sandbox mode (changes require a restart of the server)" | ||
| }, | ||
| { | ||
| key: "system.auth.ldap.url", | ||
| type: "string", | ||
| value: "", | ||
| description: "LDAP server URL (changes require a restart of the server)" | ||
| }, | ||
| { | ||
| key: "system.auth.ldap.bindDN", | ||
| type: "string", | ||
| value: "", | ||
| description: "LDAP bind DN (changes require a restart of the server)", | ||
| onlyAdmin: true | ||
| }, | ||
| { | ||
| key: "system.auth.ldap.bindCredentials", | ||
| type: "string", | ||
| value: "", | ||
| description: "LDAP bind password (changes require a restart of the server)", | ||
| onlyAdmin: true | ||
| }, | ||
| { | ||
| key: "system.auth.ldap.searchBase", | ||
| type: "string", | ||
| value: "", | ||
| description: "LDAP user search base (changes require a restart of the server)" | ||
| }, | ||
| { | ||
| key: "system.auth.ldap.searchFilter", | ||
| type: "string", | ||
| value: "(uid={{username}})", | ||
| description: "LDAP search filter template (changes require a restart of the server)" | ||
| }, | ||
| { | ||
| key: "system.auth.saml.entryPoint", | ||
| type: "string", | ||
| value: "", | ||
| description: "SAML identity provider entry point (changes require a restart of the server)" | ||
| }, | ||
| { | ||
| key: "system.auth.saml.issuer", | ||
| type: "string", | ||
| value: "", | ||
| description: "SAML service provider issuer (changes require a restart of the server)" | ||
| }, | ||
| { | ||
| key: "system.auth.saml.cert", | ||
| type: "text", | ||
| value: "", | ||
| description: "SAML identity provider certificate (changes require a restart of the server)", | ||
| onlyAdmin: true | ||
| }, | ||
| { | ||
| key: "system.auth.saml.callbackUrl", | ||
| type: "string", | ||
| value: "", | ||
| description: "SAML callback URL (changes require a restart of the server)" | ||
| }, | ||
| ]; | ||
|
|
||
| /** @type {import('sequelize-cli').Migration} */ | ||
| module.exports = { | ||
| async up(queryInterface, Sequelize) { | ||
| await queryInterface.bulkInsert("setting", | ||
| settings.map((setting) => ({ | ||
| ...setting, | ||
| createdAt: new Date(), | ||
| updatedAt: new Date(), | ||
| })), | ||
| ); | ||
| }, | ||
|
|
||
| async down(queryInterface, Sequelize) { | ||
| await queryInterface.bulkDelete("setting", { | ||
| key: settings.map((setting) => setting.key) | ||
| }, {}); | ||
| } | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,134 @@ | ||
| "use strict"; | ||
|
|
||
| const previousTermsTemplate = { | ||
| ops: [ | ||
| { insert: "Einwilligungserklärung\n", attributes: { header: 2 } }, | ||
| { insert: "Ich willige ein, dass meine personenbezogenen Daten (z. B. Name, Benutzerkennung, eingereichte Inhalte, Bewertungen, Interaktionen und IP-Adresse) zum Zweck der Durchführung der Anwendung verarbeitet werden dürfen. Zusätzlich willige ich in die Verarbeitung meiner personenbezogenen Daten für Forschungszwecke ein, sofern ich separat zugestimmt habe.\n\n" }, | ||
|
|
||
| { insert: "Informationen und Datenschutzerklärung\n", attributes: { header: 2 } }, | ||
| { insert: "Die Datenverarbeitung erfolgt unter Beachtung geltender Datenschutzgesetze (z. B. DSGVO). Alle Daten werden ausschließlich zu den im Informationsblatt beschriebenen Zwecken verwendet.\n\n" }, | ||
|
|
||
| { insert: "1. Bezeichnung der Verarbeitungstätigkeit\n", attributes: { header: 3 } }, | ||
| { insert: "Verarbeitung personenbezogener Daten zur Durchführung der Anwendung und optional zur wissenschaftlichen Auswertung von Nutzungsverhalten und Feedback.\n\n" }, | ||
|
|
||
| { insert: "2. Datenverantwortlicher\n", attributes: { header: 3 } }, | ||
| { insert: "Verantwortliche Organisation:\n", attributes: { bold: true } }, | ||
| { insert: "[Name der Organisation]\n[Adresse]\n[E-Mail-Adresse oder Kontaktlink]\n\n" }, | ||
|
|
||
| { insert: "3. Datenschutzbeauftragter\n", attributes: { header: 3 } }, | ||
| { insert: "[Name/Titel des Datenschutzbeauftragten]\n[Organisation oder Kontaktstelle]\n[Adresse]\n[E-Mail oder Datenschutzlink]\n\n" }, | ||
|
|
||
| { insert: "4. Zweck(e) und Rechtsgrundlage(n) der Verarbeitung\n", attributes: { header: 3 } }, | ||
| { insert: "Zwecke:\n", attributes: { bold: true } }, | ||
| { insert: "Bewertung und Durchführung der Anwendung\n", attributes: { list: "bullet" } }, | ||
| { insert: "Eindeutige Zuordnung über Benutzerkennungen\n", attributes: { list: "bullet" } }, | ||
| { insert: "Nutzung anonymisierter Daten für Forschung und Publikation (bei Zustimmung)\n", attributes: { list: "bullet" } }, | ||
| { insert: "Analyse des Nutzungsverhaltens (z. B. Klicks, Navigation, Zeitstempel)\n", attributes: { list: "bullet" } }, | ||
| { insert: "Technisch notwendige IP-Verarbeitung (nicht gespeichert)\n", attributes: { list: "bullet" } }, | ||
| { insert: "\nRechtsgrundlage:\n", attributes: { bold: true } }, | ||
| { insert: "Art. 6 Abs. 1 lit. a DSGVO (Einwilligung)\n\n" }, | ||
|
|
||
| { insert: "5. Dauer der Speicherung\n", attributes: { header: 3 } }, | ||
| { insert: "Personenbezogene Daten werden für einen definierten Zeitraum (z. B. zwei Jahre nach Abschluss) gespeichert und anschließend gelöscht oder anonymisiert.\n\n" }, | ||
|
|
||
| { insert: "6. Rechte der betroffenen Personen\n", attributes: { header: 3 } }, | ||
| { insert: "Recht auf Auskunft\n", attributes: { list: "bullet" } }, | ||
| { insert: "Recht auf Berichtigung\n", attributes: { list: "bullet" } }, | ||
| { insert: "Recht auf Löschung oder Einschränkung\n", attributes: { list: "bullet" } }, | ||
| { insert: "Recht auf Widerspruch\n", attributes: { list: "bullet" } }, | ||
| { insert: "Recht auf Datenübertragbarkeit\n", attributes: { list: "bullet" } }, | ||
| { insert: "Beschwerderecht bei einer Aufsichtsbehörde\n", attributes: { list: "bullet" } }, | ||
| { insert: "\n" }, | ||
|
|
||
| { insert: "7. Widerrufsrecht\n", attributes: { header: 3 } }, | ||
| { insert: "Die Einwilligung kann jederzeit widerrufen werden. Die Rechtmäßigkeit der bis dahin erfolgten Verarbeitung bleibt unberührt. Nach Anonymisierung ist keine nachträgliche Löschung mehr möglich.\n\n" }, | ||
|
|
||
| { insert: "8. Veröffentlichung anonymisierter Daten\n", attributes: { header: 3 } }, | ||
| { insert: "Anonymisierte Ergebnisse können in wissenschaftlichen Publikationen veröffentlicht werden. Die Daten werden unter einer offenen Lizenz (z. B. CC BY-NC 4.0) in geeigneten Repositorien veröffentlicht.\n" } | ||
| ] | ||
| }; | ||
|
|
||
| const updatedTermsTemplate = { | ||
| ops: [ | ||
| { insert: "Einwilligungserklärung\n", attributes: { header: 2 } }, | ||
| { insert: "Ich willige ein, dass meine personenbezogenen Daten (z. B. Name, Benutzerkennung, eingereichte Inhalte, Bewertungen, Interaktionen und IP-Adresse) zum Zweck der Durchführung der Anwendung verarbeitet werden dürfen. Zusätzlich willige ich in die Verarbeitung meiner personenbezogenen Daten für Forschungszwecke ein, sofern ich separat zugestimmt habe.\n\n" }, | ||
|
|
||
| { insert: "Informationen und Datenschutzerklärung\n", attributes: { header: 2 } }, | ||
| { insert: "Die Datenverarbeitung erfolgt unter Beachtung geltender Datenschutzgesetze (z. B. DSGVO). Alle Daten werden ausschließlich zu den im Informationsblatt beschriebenen Zwecken verwendet.\n\n" }, | ||
|
|
||
| { insert: "1. Bezeichnung der Verarbeitungstätigkeit\n", attributes: { header: 3 } }, | ||
| { insert: "Verarbeitung personenbezogener Daten zur Durchführung der Anwendung und optional zur wissenschaftlichen Auswertung von Nutzungsverhalten und Feedback.\n\n" }, | ||
|
|
||
| { insert: "2. Datenverantwortlicher\n", attributes: { header: 3 } }, | ||
| { insert: "Verantwortliche Organisation:\n", attributes: { bold: true } }, | ||
| { insert: "[Name der Organisation]\n[Adresse]\n[E-Mail-Adresse oder Kontaktlink]\n\n" }, | ||
|
|
||
| { insert: "3. Datenschutzbeauftragter\n", attributes: { header: 3 } }, | ||
| { insert: "[Name/Titel des Datenschutzbeauftragten]\n[Organisation oder Kontaktstelle]\n[Adresse]\n[E-Mail oder Datenschutzlink]\n\n" }, | ||
|
|
||
| { insert: "4. Zweck(e) und Rechtsgrundlage(n) der Verarbeitung\n", attributes: { header: 3 } }, | ||
| { insert: "Zwecke:\n", attributes: { bold: true } }, | ||
| { insert: "Bewertung und Durchführung der Anwendung\n", attributes: { list: "bullet" } }, | ||
| { insert: "Eindeutige Zuordnung über Benutzerkennungen\n", attributes: { list: "bullet" } }, | ||
| { insert: "Sicherstellung der Kontosicherheit durch Zwei-Faktor-Authentifizierung (2FA), einschließlich Versand einmaliger Verifizierungscodes per E-Mail\n", attributes: { list: "bullet" } }, | ||
| { insert: "Nutzung anonymisierter Daten für Forschung und Publikation (bei Zustimmung)\n", attributes: { list: "bullet" } }, | ||
| { insert: "Analyse des Nutzungsverhaltens (z. B. Klicks, Navigation, Zeitstempel)\n", attributes: { list: "bullet" } }, | ||
| { insert: "Technisch notwendige IP-Verarbeitung (nicht gespeichert)\n", attributes: { list: "bullet" } }, | ||
| { insert: "\nRechtsgrundlage:\n", attributes: { bold: true } }, | ||
| { insert: "Art. 6 Abs. 1 lit. a DSGVO (Einwilligung)\n\n" }, | ||
|
|
||
| { insert: "4a. Hinweise zur E-Mail-Verifizierung (2FA)\n", attributes: { header: 3 } }, | ||
| { insert: "Wenn E-Mail-2FA aktiviert ist, werden bei der Anmeldung einmalige Sicherheitscodes an die hinterlegte E-Mail-Adresse gesendet.\n" }, | ||
| { insert: "Verarbeitet werden dabei insbesondere:\n", attributes: { bold: true } }, | ||
| { insert: "E-Mail-Adresse\n", attributes: { list: "bullet" } }, | ||
| { insert: "Zeitpunkt des Versands und Ablaufzeitpunkt des Codes\n", attributes: { list: "bullet" } }, | ||
| { insert: "Technische Metadaten zur Missbrauchs- und Fehlerprävention\n", attributes: { list: "bullet" } }, | ||
| { insert: "Die Codes werden ausschließlich zur Anmeldung und Kontosicherheit verwendet, nicht für Werbung oder Newsletter.\n\n" }, | ||
|
|
||
| { insert: "5. Dauer der Speicherung\n", attributes: { header: 3 } }, | ||
| { insert: "Personenbezogene Daten werden für einen definierten Zeitraum (z. B. zwei Jahre nach Abschluss) gespeichert und anschließend gelöscht oder anonymisiert.\n\n" }, | ||
|
|
||
| { insert: "6. Rechte der betroffenen Personen\n", attributes: { header: 3 } }, | ||
| { insert: "Recht auf Auskunft\n", attributes: { list: "bullet" } }, | ||
| { insert: "Recht auf Berichtigung\n", attributes: { list: "bullet" } }, | ||
| { insert: "Recht auf Löschung oder Einschränkung\n", attributes: { list: "bullet" } }, | ||
| { insert: "Recht auf Widerspruch\n", attributes: { list: "bullet" } }, | ||
| { insert: "Recht auf Datenübertragbarkeit\n", attributes: { list: "bullet" } }, | ||
| { insert: "Beschwerderecht bei einer Aufsichtsbehörde\n", attributes: { list: "bullet" } }, | ||
| { insert: "\n" }, | ||
|
|
||
| { insert: "7. Widerrufsrecht\n", attributes: { header: 3 } }, | ||
| { insert: "Die Einwilligung kann jederzeit widerrufen werden. Die Rechtmäßigkeit der bis dahin erfolgten Verarbeitung bleibt unberührt. Nach Anonymisierung ist keine nachträgliche Löschung mehr möglich.\n\n" }, | ||
|
|
||
| { insert: "8. Veröffentlichung anonymisierter Daten\n", attributes: { header: 3 } }, | ||
| { insert: "Anonymisierte Ergebnisse können in wissenschaftlichen Publikationen veröffentlicht werden. Die Daten werden unter einer offenen Lizenz (z. B. CC BY-NC 4.0) in geeigneten Repositorien veröffentlicht.\n" } | ||
| ] | ||
| }; | ||
|
|
||
| module.exports = { | ||
| async up(queryInterface, Sequelize) { | ||
| await queryInterface.bulkUpdate( | ||
| "setting", | ||
| { | ||
| value: updatedTermsTemplate, | ||
| updatedAt: new Date(), | ||
| }, | ||
| { | ||
| key: "app.register.terms", | ||
| }, | ||
| ); | ||
| }, | ||
|
|
||
| async down(queryInterface, Sequelize) { | ||
| await queryInterface.bulkUpdate( | ||
| "setting", | ||
| { | ||
| value: previousTermsTemplate, | ||
| updatedAt: new Date(), | ||
| }, | ||
| { | ||
| key: "app.register.terms", | ||
| }, | ||
| ); | ||
| }, | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -555,6 +555,20 @@ module.exports = (sequelize, DataTypes) => { | |
| emailVerificationToken: DataTypes.STRING, | ||
| lastPasswordResetEmailSent: DataTypes.DATE, | ||
| lastVerificationEmailSent: DataTypes.DATE, | ||
| // Email OTP for 2FA | ||
| twoFactorOtp: DataTypes.STRING, | ||
| twoFactorOtpExpiresAt: DataTypes.DATE, | ||
| // Multi-method 2FA configuration | ||
| twoFactorMethods: { | ||
| type: DataTypes.JSON, | ||
| defaultValue: [], | ||
| }, | ||
| // TOTP for 2FA | ||
| totpSecret: DataTypes.STRING, | ||
| // External login method identifiers | ||
| orcidId: DataTypes.STRING, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [question] should we add unique: true here too?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We usually set the extra rules in the migration files; here we do not need to add |
||
| ldapUsername: DataTypes.STRING, | ||
| samlNameId: DataTypes.STRING, | ||
| }, | ||
| { | ||
| sequelize, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[QUESTION] should this also be in the down function? changing back the column "email" as it was
Also, why are we allowing null for emails?