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

IAM 978 User invite - send email for identity creation with recovery code and link #403

Merged
merged 10 commits into from
Sep 10, 2024
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,20 @@ This is the Admin UI for the Canonical Identity Platform.
middleware is enabled default to `true`
- `AUTHENTICATION_ENABLED`: flag defining if the OAuth authentication middleware
is enabled, default to `false`
- `OIDC_ISSUER`: URL of the OIDC provider
- `OIDC_ISSUER`: URL of the OIDC provider
- `OAUTH2_CLIENT_ID`: OAuth2 client ID used for authentication purposes
- `OAUTH2_CLIENT_SECRET`: OAuth2 client secret used for authentication purposes
- `OAUTH2_REDIRECT_URI`: URI used by the Oauth2 provider for the redirecting callback
- `OAUTH2_CODEGRANT_SCOPES`: OAuth2 scopes, defaults to `openid,offline_access`
- `OAUTH2_AUTH_COOKIES_ENCRYPTION_KEY`: 32 bytes string used for encrypting cookies
- `ACCESS_TOKEN_VERIFICATION_STRATEGY`: OAuth2 verification startegy, one of `jwks` or `userinfo``
- `MAIL_HOST`: host of the mail server (required)
- `MAIL_PORT`: port exposed by the mail server (required)
- `MAIL_USERNAME`: username to use for the simple authentication on the mail server (if present, both username and
password are used)
- `MAIL_PASSWORD`: password to use for the simple authentication on the mail server
- `MAIL_FROM_ADDRESS`: email address sending the email (required)
- `MAIL_SEND_TIMEOUT_SECONDS`: timeout used to send emails (defaults to 15 seconds)

## Development setup

Expand Down
5 changes: 4 additions & 1 deletion cmd/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
k8s "github.com/canonical/identity-platform-admin-ui/internal/k8s"
ik "github.com/canonical/identity-platform-admin-ui/internal/kratos"
"github.com/canonical/identity-platform-admin-ui/internal/logging"
"github.com/canonical/identity-platform-admin-ui/internal/mail"
"github.com/canonical/identity-platform-admin-ui/internal/monitoring/prometheus"
io "github.com/canonical/identity-platform-admin-ui/internal/oathkeeper"
"github.com/canonical/identity-platform-admin-ui/internal/openfga"
Expand Down Expand Up @@ -164,9 +165,11 @@ func serve() {
hydraAdminClient,
)

mailConfig := mail.NewConfig(specs.MailHost, specs.MailPort, specs.MailUsername, specs.MailPassword, specs.MailFromAddress, specs.MailSendTimeoutSeconds)

ollyConfig := web.NewO11yConfig(tracer, monitor, logger)

routerConfig := web.NewRouterConfig(specs.ContextPath, specs.PayloadValidationEnabled, idpConfig, schemasConfig, rulesConfig, uiConfig, externalConfig, oauth2Config, ollyConfig)
routerConfig := web.NewRouterConfig(specs.ContextPath, specs.PayloadValidationEnabled, idpConfig, schemasConfig, rulesConfig, uiConfig, externalConfig, oauth2Config, mailConfig, ollyConfig)

router := web.NewRouter(routerConfig, wpool)

Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ require (
github.com/prometheus/common v0.44.0 // indirect
github.com/prometheus/procfs v0.11.1 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/wneessen/go-mail v0.4.4 // indirect
go.opentelemetry.io/otel/metric v1.19.0 // indirect
go.opentelemetry.io/proto/otlp v1.0.0 // indirect
go.uber.org/multierr v1.10.0 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,8 @@ github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 h1:nrZ3ySNYwJ
github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80/go.mod h1:iFyPdL66DjUD96XmzVL3ZntbzcflLnznH0fr99w5VqE=
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/wneessen/go-mail v0.4.4 h1:rI8wJzPYymUpUth87vFV3k313bmnid4v+FwhBAYYLFM=
github.com/wneessen/go-mail v0.4.4/go.mod h1:zxOlafWCP/r6FEhAaRgH4IC1vg2YXxO0Nar9u0IScZ8=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
Expand Down
7 changes: 7 additions & 0 deletions internal/config/specs.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,11 @@ type EnvSpec struct {
PayloadValidationEnabled bool `envconfig:"payload_validation_enabled" default:"true"`

OpenFGAWorkersTotal int `envconfig:"openfga_workers_total" default:"150"`

MailHost string `envconfig:"MAIL_HOST" required:"true"`
MailPort int `envconfig:"MAIL_PORT" required:"true"`
MailUsername string `envconfig:"MAIL_USERNAME"`
MailPassword string `envconfig:"MAIL_PASSWORD"`
MailFromAddress string `envconfig:"MAIL_FROM_ADDRESS" required:"true"`
MailSendTimeoutSeconds int `envconfig:"MAIL_SEND_TIMEOUT_SECONDS" default:"15"`
}
85 changes: 85 additions & 0 deletions internal/mail/html/user-invite.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<!-- Copyright 2024 Canonical Ltd. -->
<!-- SPDX-License-Identifier: AGPL-3.0 -->

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta content="width=device-width, initial-scale=1.0" name="viewport">
<title>Verify Your Account</title>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f6f6f6;
color: #333;
margin: 0;
padding: 0;
}

.container {
width: 100%;
max-width: 600px;
margin: 20px auto;
background-color: #ffffff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}

.header {
text-align: center;
padding: 20px 0;
}

.header h1 {
color: #007BFF;
margin: 0;
}

.content {
text-align: center;
}

.content p {
font-size: 16px;
line-height: 1.5;
margin: 20px 0;
}

.verify-button {
display: inline-block;
background-color: #007BFF;
color: #ffffff;
text-decoration: none;
padding: 10px 20px;
border-radius: 5px;
font-size: 18px;
margin-top: 20px;
}

.footer {
text-align: center;
font-size: 12px;
color: #666;
margin-top: 20px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>Verify Your Account</h1>
</div>
<div class="content">
<p>Hello,</p>
<p>Your account with the email address <strong>{{ .Email }}</strong> was recently created. To complete your
registration, click the button below:</p>
<p>Verification code <span><strong>{{ .RecoveryCode }}</strong></span></p>
<a class="verify-button" href="{{ .InviteUrl }}">Verify Account</a>
</div>
<div class="footer">
<p>&copy;Copyright 2024 Canonical Ltd.</p>
</div>
</div>
</body>
</html>
19 changes: 19 additions & 0 deletions internal/mail/interfaces.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Copyright 2024 Canonical Ltd.
// SPDX-License-Identifier: AGPL-3.0

package mail

import (
"context"
"html/template"

mail2 "github.com/wneessen/go-mail"
)

type EmailServiceInterface interface {
Send(context.Context, string, string, *template.Template, any) error
}

type MailClientInterface interface {
DialAndSendWithContext(context.Context, ...*mail2.Msg) error
}
105 changes: 105 additions & 0 deletions internal/mail/service.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// Copyright 2024 Canonical Ltd.
// SPDX-License-Identifier: AGPL-3.0

package mail

import (
"context"
"html/template"
"time"

"github.com/wneessen/go-mail"
"go.opentelemetry.io/otel/trace"

"github.com/canonical/identity-platform-admin-ui/internal/logging"
"github.com/canonical/identity-platform-admin-ui/internal/monitoring"
)

type Config struct {
Host string `validate:"required"`
Port int `validate:"required"`
Username string
Password string
FromAddress string `validate:"required"`
SendTimeout time.Duration
}

func NewConfig(host string, port int, username, password, from string, sendTimeout int) *Config {
c := new(Config)

c.Host = host
c.Port = port
c.Username = username
c.Password = password
c.FromAddress = from
c.SendTimeout = time.Duration(sendTimeout) * time.Second

return c
}

type EmailService struct {
from string
client MailClientInterface

tracer trace.Tracer
monitor monitoring.MonitorInterface
logger logging.LoggerInterface
}

func (e *EmailService) Send(ctx context.Context, to, subject string, template *template.Template, templateArgs any) error {
ctx, span := e.tracer.Start(ctx, "mail.EmailService.Send")
defer span.End()

msg := mail.NewMsg()

if err := msg.From(e.from); err != nil {
return err
}

if err := msg.SetBodyHTMLTemplate(template, templateArgs); err != nil {
return err
}

if err := msg.To(to); err != nil {
return err
}

msg.Subject(subject)

return e.client.DialAndSendWithContext(ctx, msg)
}

func NewEmailService(config *Config, tracer trace.Tracer, monitor monitoring.MonitorInterface, logger logging.LoggerInterface) *EmailService {
s := new(EmailService)
s.from = config.FromAddress

var err error
mailOpts := []mail.Option{
mail.WithPort(config.Port),
mail.WithTLSPolicy(mail.TLSOpportunistic),
BarcoMasile marked this conversation as resolved.
Show resolved Hide resolved
mail.WithTimeout(config.SendTimeout),
}

// treat smtp connection as authenticated only if username is passed
if config.Username != "" {
mailOpts = append(
mailOpts,
[]mail.Option{mail.WithSMTPAuth(mail.SMTPAuthPlain), mail.WithUsername(config.Username), mail.WithPassword(config.Password)}...,
)
}

s.client, err = mail.NewClient(
config.Host,
mailOpts...,
)

if err != nil {
logger.Fatalf("failed to create email client: %s", err)
}

s.monitor = monitor
s.tracer = tracer
s.logger = logger

return s
}
Loading
Loading