Show the source device in login code mails

This commit is contained in:
Jonas Lochmann 2021-12-27 01:00:00 +01:00
parent add8787624
commit 3a59743de9
No known key found for this signature in database
GPG key ID: 8B8C9AEE10FA5B36
12 changed files with 190 additions and 209 deletions

View file

@ -24,7 +24,8 @@ see [this JSON schema](../schema/sendmaillogincoderequest.md)
```
{
"mail": "test@timelimit.io",
"locale": "de"
"locale": "de",
"deviceAuthToken": "1234abcde"
}
```
@ -34,6 +35,8 @@ If the request body is malformed or the mail address is invalid: HTTP status cod
If the rate limit was exceeded: HTTP status code 429 Too Many Requests
If a deviceAuthToken was sent which is invalid: HTTP status code 401 Unauthorized
If a whitelist was configured and the mail address is not within it: ``{"mailAddressNotWhitelisted": true}``
If a blacklist was configured and the mail server is within it: ``{"mailServerBlacklisted": true}``

View file

@ -6,6 +6,9 @@
},
"locale": {
"type": "string"
},
"deviceAuthToken": {
"type": "string"
}
},
"additionalProperties": false,

View file

@ -0,0 +1,15 @@
# Untitled string in SendMailLoginCodeRequest Schema
```txt
https://timelimit.io/SendMailLoginCodeRequest#/properties/deviceAuthToken
```
| Abstract | Extensible | Status | Identifiable | Custom Properties | Additional Properties | Access Restrictions | Defined In |
| :------------------ | :--------- | :------------- | :---------------------- | :---------------- | :-------------------- | :------------------ | :--------------------------------------------------------------------------------------------------- |
| Can be instantiated | No | Unknown status | Unknown identifiability | Forbidden | Allowed | none | [SendMailLoginCodeRequest.schema.json*](SendMailLoginCodeRequest.schema.json "open original schema") |
## deviceAuthToken Type
`string`

View file

@ -16,10 +16,11 @@ https://timelimit.io/SendMailLoginCodeRequest
# SendMailLoginCodeRequest Properties
| Property | Type | Required | Nullable | Defined by |
| :---------------- | :------- | :------- | :------------- | :------------------------------------------------------------------------------------------------------------------------------------------- |
| [mail](#mail) | `string` | Required | cannot be null | [SendMailLoginCodeRequest](sendmaillogincoderequest-properties-mail.md "https://timelimit.io/SendMailLoginCodeRequest#/properties/mail") |
| [locale](#locale) | `string` | Required | cannot be null | [SendMailLoginCodeRequest](sendmaillogincoderequest-properties-locale.md "https://timelimit.io/SendMailLoginCodeRequest#/properties/locale") |
| Property | Type | Required | Nullable | Defined by |
| :---------------------------------- | :------- | :------- | :------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [mail](#mail) | `string` | Required | cannot be null | [SendMailLoginCodeRequest](sendmaillogincoderequest-properties-mail.md "https://timelimit.io/SendMailLoginCodeRequest#/properties/mail") |
| [locale](#locale) | `string` | Required | cannot be null | [SendMailLoginCodeRequest](sendmaillogincoderequest-properties-locale.md "https://timelimit.io/SendMailLoginCodeRequest#/properties/locale") |
| [deviceAuthToken](#deviceauthtoken) | `string` | Optional | cannot be null | [SendMailLoginCodeRequest](sendmaillogincoderequest-properties-deviceauthtoken.md "https://timelimit.io/SendMailLoginCodeRequest#/properties/deviceAuthToken") |
## mail
@ -57,4 +58,22 @@ https://timelimit.io/SendMailLoginCodeRequest
`string`
## deviceAuthToken
`deviceAuthToken`
* is optional
* Type: `string`
* cannot be null
* defined in: [SendMailLoginCodeRequest](sendmaillogincoderequest-properties-deviceauthtoken.md "https://timelimit.io/SendMailLoginCodeRequest#/properties/deviceAuthToken")
### deviceAuthToken Type
`string`
# SendMailLoginCodeRequest Definitions

View file

@ -2,8 +2,9 @@
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
<head>
<title> </title>
<!--[if !mso]><!-- -->
<title>
</title>
<!--[if !mso]><!-->
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<!--<![endif]-->
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
@ -13,18 +14,6 @@
padding: 0;
}
.ReadMsgBody {
width: 100%;
}
.ExternalClass {
width: 100%;
}
.ExternalClass * {
line-height: 100%;
}
body {
margin: 0;
padding: 0;
@ -53,29 +42,19 @@
margin: 13px 0;
}
</style>
<!--[if !mso]><!-->
<style type="text/css">
@media only screen and (max-width:480px) {
@-ms-viewport {
width: 320px;
}
@viewport {
width: 320px;
}
}
</style>
<!--<![endif]-->
<!--[if mso]>
<noscript>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG/>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
</noscript>
<![endif]-->
<!--[if lte mso 11]>
<style type="text/css">
.outlook-group-fix { width:100% !important; }
.mj-outlook-group-fix { width:100% !important; }
</style>
<![endif]-->
<style type="text/css">
@ -86,224 +65,126 @@
}
}
</style>
<style media="screen and (min-width:480px)">
.moz-text-html .mj-column-per-100 {
width: 100% !important;
max-width: 100%;
}
</style>
<style type="text/css">
</style>
</head>
<body>
<body style="word-spacing:normal;">
<div style="">
<!--[if mso | IE]>
<table
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
>
<tr>
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
<![endif]-->
<div style="background:#009688;background-color:#009688;Margin:0px auto;max-width:600px;">
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#009688" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:#009688;background-color:#009688;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#009688;background-color:#009688;width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;vertical-align:top;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tr>
<td
class="" style="vertical-align:top;width:600px;"
>
<![endif]-->
<div class="mj-column-per-100 outlook-group-fix" style="font-size:13px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:20px;line-height:1;text-align:left;color:#ffffff;"> TimeLimit </div>
</td>
</tr>
<tbody>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:20px;line-height:1;text-align:left;color:#ffffff;">TimeLimit</div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<table
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
>
<tr>
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
<![endif]-->
<div style="Margin:0px auto;max-width:600px;">
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;vertical-align:top;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tr>
<td
class="" style="vertical-align:top;width:600px;"
>
<![endif]-->
<div class="mj-column-per-100 outlook-group-fix" style="font-size:13px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;line-height:1;text-align:left;color:#000000;">
<p>
<%= introtext %>
</p>
<p><b><%= code %></b></p>
<p>
<%= outrotext %>
</p>
</div>
</td>
</tr>
<tbody>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;line-height:1;text-align:left;color:#000000;">
<p> <%= introtext %> </p>
<p><b><%= code %></b></p>
<p><%= outrotext %></p> <% if (deviceName !== null) { -%> <p><%= deviceNameIntro %> <b><%= deviceName %></b> <%= deviceNameOutro %></p> <% } -%>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<table
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
>
<tr>
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
<![endif]-->
<div style="Margin:0px auto;max-width:600px;">
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;vertical-align:top;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tr>
<td
class="" style="vertical-align:top;width:600px;"
>
<![endif]-->
<div class="mj-column-per-100 outlook-group-fix" style="font-size:13px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tr>
<td style="font-size:0px;padding:10px 25px;word-break:break-word;">
<p style="border-top:dashed 1px lightgrey;font-size:1;margin:0px auto;width:100%;"> </p>
<!--[if mso | IE]>
<table
align="center" border="0" cellpadding="0" cellspacing="0" style="border-top:dashed 1px lightgrey;font-size:1;margin:0px auto;width:550px;" role="presentation" width="550px"
>
<tr>
<td style="height:0;line-height:0;">
&nbsp;
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
<tbody>
<tr>
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<p style="border-top:dashed 1px lightgrey;font-size:1px;margin:0px auto;width:100%;">
</p>
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" style="border-top:dashed 1px lightgrey;font-size:1px;margin:0px auto;width:550px;" role="presentation" width="550px" ><tr><td style="height:0;line-height:0;"> &nbsp;
</td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<table
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
>
<tr>
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
<![endif]-->
<div style="Margin:0px auto;max-width:600px;">
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;vertical-align:top;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tr>
<td
class="" style="vertical-align:top;width:600px;"
>
<![endif]-->
<div class="mj-column-per-100 outlook-group-fix" style="font-size:13px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;line-height:1;text-align:left;color:#000000;">
<p> You got this mail because your mail address was entered at the TimeLimit App for signing in. If you did not request this mail, then you can ignore it. If you have got any questions, then you can reply to this messagge. </p>
<p>
Sie erhalten diese Nachricht, da Ihre E-Mail-Adresse in der TimeLimit-App zum Anmelden eingegeben wurde. Wenn Sie diese E-Mail nicht angefordert haben, können Sie diese ignorieren. Falls Sie Fragen haben können Sie einfach auf
diese E-Mail antworten. </p>
<p> &copy;
<%= mailimprint %>
</p>
</div>
</td>
</tr>
<tbody>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;line-height:1;text-align:left;color:#000000;">
<p> You got this mail because your mail address was entered at the TimeLimit App for signing in. If you did not request this mail, then you can ignore it. If you have got any questions, then you can reply to this messagge. </p>
<p> Sie erhalten diese Nachricht, da Ihre E-Mail-Adresse in der TimeLimit-App zum Anmelden eingegeben wurde. Wenn Sie diese E-Mail nicht angefordert haben, können Sie diese ignorieren. Falls Sie Fragen haben können Sie einfach auf diese E-Mail antworten. </p>
<p> &copy; <%= mailimprint %> </p>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
<!--[if mso | IE]></td></tr></table><![endif]-->
</div>
</body>

View file

@ -13,6 +13,9 @@
</p>
<p><b><%= code %></b></p>
<p><%= outrotext %></p>
<% if (deviceName !== null) { -%>
<p><%= deviceNameIntro %> <b><%= deviceName %></b> <%= deviceNameOutro %></p>
<% } -%>
</mj-text>
</mj-column>
</mj-section>

View file

@ -4,6 +4,10 @@
<%- outrotext %>
<% if (deviceName !== null) { -%>
<%- deviceNameIntro %> <%- deviceName %> <%- deviceNameOutro %>
<% } -%>
----------------------
You got this mail because your mail address was entered at the TimeLimit App for signing in.

View file

@ -1,6 +1,6 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 - 2020 Jonas Lochmann
* Copyright (C) 2019 - 2021 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
@ -48,6 +48,7 @@ export const createAuthRouter = (database: Database) => {
} else {
const { mailLoginToken } = await sendLoginCode({
mail,
deviceAuthToken: req.body.deviceAuthToken,
locale: req.body.locale,
database
})

View file

@ -132,6 +132,7 @@ export interface RequestWithAuthToken {
export interface SendMailLoginCodeRequest {
mail: string
locale: string
deviceAuthToken?: string
}
export interface SignInByMailCodeRequest {

View file

@ -2966,6 +2966,9 @@ export const isSendMailLoginCodeRequest: (value: object) => value is SendMailLog
},
"locale": {
"type": "string"
},
"deviceAuthToken": {
"type": "string"
}
},
"additionalProperties": false,

View file

@ -1,6 +1,6 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 - 2020 Jonas Lochmann
* Copyright (C) 2019 - 2021 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Forbidden, Gone, TooManyRequests } from 'http-errors'
import { Forbidden, Gone, TooManyRequests, Unauthorized } from 'http-errors'
import { Database } from '../../database'
import { sendAuthenticationMail } from '../../util/mail'
import { areWordSequencesEqual, randomWords } from '../../util/random-words'
@ -23,12 +23,52 @@ import { checkMailSendLimit } from '../../util/ratelimit-authmail'
import { generateAuthToken } from '../../util/token'
import { createAuthTokenByMailAddress } from './index'
export const sendLoginCode = async ({ mail, locale, database }: {
export const sendLoginCode = async ({ mail, deviceAuthToken, locale, database }: {
mail: string
deviceAuthToken?: string
locale: string
database: Database
// no transaction here because this is directly called from an API endpoint
}): Promise<{ mailLoginToken: string }> => {
let deviceName = null
if (deviceAuthToken !== undefined) {
const info = await database.transaction(async (transaction) => {
const deviceEntryUnsafe = await database.device.findOne({
where: { deviceAuthToken },
attributes: ['familyId', 'name'],
transaction
})
if (!deviceEntryUnsafe) {
throw new Unauthorized()
}
const deviceEntry = {
familyId: deviceEntryUnsafe.familyId,
name: deviceEntryUnsafe.name
}
const userEntryCounter = await database.user.count({
where: {
familyId: deviceEntry.familyId,
mail
},
transaction
})
if (userEntryCounter === 1) {
return { deviceName: deviceEntry.name }
} else {
// do not show the device name if it is from another family
// otherwise third parties could chose a part of the content of the mail
return { deviceName: null }
}
})
deviceName = info.deviceName
}
try {
await checkMailSendLimit(mail)
} catch (ex) {
@ -41,7 +81,8 @@ export const sendLoginCode = async ({ mail, locale, database }: {
await sendAuthenticationMail({
receiver: mail,
code,
locale
locale,
deviceName
})
await database.transaction(async (transaction) => {

View file

@ -1,6 +1,6 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 - 2020 Jonas Lochmann
* Copyright (C) 2019 - 2021 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
@ -36,7 +36,11 @@ const email = new Email({
}
})
export const sendAuthenticationMail = async ({ receiver, code, locale }: {receiver: string, code: string, locale: string}) => {
export const sendAuthenticationMail = async ({
receiver, code, locale, deviceName
}: {
receiver: string, code: string, locale: string, deviceName: string | null
}) => {
await email.send({
template: join(__dirname, '../../other/mail/login'),
message: {
@ -47,7 +51,10 @@ export const sendAuthenticationMail = async ({ receiver, code, locale }: {receiv
introtext: locale === 'de' ? 'Geben Sie zum Authentifizieren folgenden Code in TimeLimit ein' : 'To authenticate, enter the following code in TimeLimit',
code,
outrotext: locale === 'de' ? 'Geben Sie diesen Code nicht an Dritte weiter.' : 'Do not share this code with third parties.',
mailimprint
mailimprint,
deviceName,
deviceNameIntro: locale === 'de' ? 'Die Anmeldung wurde am Gerät' : 'The login was attempted at the device',
deviceNameOutro: locale === 'de' ? 'versucht.' : '.'
}
})
}