forked from Nixius/authelia
Compare commits
8 Commits
c613dc0863
...
d2c8327d8c
| Author | SHA1 | Date |
|---|---|---|
|
|
d2c8327d8c | |
|
|
7c9d40f538 | |
|
|
0851d6f952 | |
|
|
e3b9511487 | |
|
|
0f802de51d | |
|
|
e3edf4bb53 | |
|
|
c92151d5cf | |
|
|
f70250adc4 |
|
|
@ -1,5 +1,5 @@
|
||||||
# Authelia – stable/done; keep out of context for ss-atlas and other work
|
# Authelia – stable/done; keep out of context for ss-atlas and other work
|
||||||
docker/authelia/
|
|
||||||
authelia-dev-config.yml
|
authelia-dev-config.yml
|
||||||
docker/mariadb/
|
docker/mariadb/
|
||||||
docker/redis/
|
docker/redis/
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
<p>Hi {{ .DisplayName }},</p>
|
<p>Hi {{ .DisplayName }},</p>
|
||||||
<p>You requested to set or reset your password for your <a href="https://bc.a250.ca">a250.ca</a> workspace.</p>
|
<p>You requested to set or reset your password for your <a href="https://bc.a250.ca">a250.ca</a> workspace.</p>
|
||||||
<p>Click the link below to choose your password. You will also need to enable two-factor authentication or a passkey.</p>
|
<p>Click the link below to choose your password. You will also need to enable two-factor authentication or a passkey.</p>
|
||||||
<p><a href="{{ replace "?token=" "/reset-password/step2?token=" (replace "%2Flogin" "/login" .LinkURL) }}">{{ .LinkText }}</a></p>
|
{{ $parts := splitList "token=" .LinkURL }}<p><a href="https://bc.a250.ca/login/reset-password/step2?token={{ index $parts 1 }}">{{ .LinkText }}</a></p>
|
||||||
<p>If you did not request this, you can safely ignore this email — no changes will be made.</p>
|
<p>If you did not request this, you can safely ignore this email — no changes will be made.</p>
|
||||||
<p style="color:#888;font-size:0.85em;">Requested from {{ .RemoteIP }}.</p>
|
<p style="color:#888;font-size:0.85em;">Requested from {{ .RemoteIP }}.</p>
|
||||||
</body>
|
</body>
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ You requested to set or reset your password for your a250.ca workspace (https://
|
||||||
|
|
||||||
Use the link below to choose your password. You will also need to enable two-factor authentication or a passkey.
|
Use the link below to choose your password. You will also need to enable two-factor authentication or a passkey.
|
||||||
|
|
||||||
{{ replace "?token=" "/reset-password/step2?token=" (replace "%2Flogin" "/login" .LinkURL) }}
|
{{ $parts := splitList "token=" .LinkURL }}https://bc.a250.ca/login/reset-password/step2?token={{ index $parts 1 }}
|
||||||
|
|
||||||
If you did not request this, you can safely ignore this email — no changes will be made.
|
If you did not request this, you can safely ignore this email — no changes will be made.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,9 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"log"
|
"log"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
@ -29,7 +31,7 @@ func (a *App) handleResendReset(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := a.triggerPasswordReset(username); err != nil {
|
if err := a.triggerPasswordReset(r, username); err != nil {
|
||||||
log.Printf("resend-reset: failed for %s: %v", username, err)
|
log.Printf("resend-reset: failed for %s: %v", username, err)
|
||||||
respondResendError(w, http.StatusInternalServerError, "failed to send email", 0)
|
respondResendError(w, http.StatusInternalServerError, "failed to send email", 0)
|
||||||
return
|
return
|
||||||
|
|
@ -53,19 +55,33 @@ func respondResendError(w http.ResponseWriter, code int, msg string, retryAfter
|
||||||
json.NewEncoder(w).Encode(body)
|
json.NewEncoder(w).Encode(body)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) triggerPasswordReset(username string) error {
|
func clientIP(r *http.Request) string {
|
||||||
|
if s := r.Header.Get("X-Forwarded-For"); s != "" {
|
||||||
|
if idx := strings.Index(s, ","); idx > 0 {
|
||||||
|
return strings.TrimSpace(s[:idx])
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(s)
|
||||||
|
}
|
||||||
|
if s := r.Header.Get("X-Real-IP"); s != "" {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
if host, _, err := net.SplitHostPort(r.RemoteAddr); err == nil {
|
||||||
|
return host
|
||||||
|
}
|
||||||
|
return r.RemoteAddr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) triggerPasswordReset(r *http.Request, username string) error {
|
||||||
|
url := a.cfg.AutheliaInternalURL + "/api/reset-password/identity/start"
|
||||||
body, _ := json.Marshal(map[string]string{"username": username})
|
body, _ := json.Marshal(map[string]string{"username": username})
|
||||||
|
|
||||||
req, err := http.NewRequest(
|
log.Printf("triggerPasswordReset: POST %s for user %q", url, username)
|
||||||
http.MethodPost,
|
|
||||||
a.cfg.AutheliaInternalURL+"/api/reset-password/identity/start",
|
req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(body))
|
||||||
bytes.NewReader(body),
|
|
||||||
)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("authelia reset build request: %w", err)
|
return fmt.Errorf("authelia reset build request: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Strip scheme from AutheliaURL to get the host for forwarding headers
|
|
||||||
externalHost := strings.TrimPrefix(strings.TrimPrefix(a.cfg.AutheliaURL, "https://"), "http://")
|
externalHost := strings.TrimPrefix(strings.TrimPrefix(a.cfg.AutheliaURL, "https://"), "http://")
|
||||||
proto := "http"
|
proto := "http"
|
||||||
if strings.HasPrefix(a.cfg.AutheliaURL, "https://") {
|
if strings.HasPrefix(a.cfg.AutheliaURL, "https://") {
|
||||||
|
|
@ -75,7 +91,7 @@ func (a *App) triggerPasswordReset(username string) error {
|
||||||
req.Header.Set("Content-Type", "application/json")
|
req.Header.Set("Content-Type", "application/json")
|
||||||
req.Header.Set("X-Forwarded-Host", externalHost)
|
req.Header.Set("X-Forwarded-Host", externalHost)
|
||||||
req.Header.Set("X-Forwarded-Proto", proto)
|
req.Header.Set("X-Forwarded-Proto", proto)
|
||||||
req.Header.Set("X-Forwarded-For", "127.0.0.1")
|
req.Header.Set("X-Forwarded-For", clientIP(r))
|
||||||
|
|
||||||
resp, err := http.DefaultClient.Do(req)
|
resp, err := http.DefaultClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -83,8 +99,19 @@ func (a *App) triggerPasswordReset(username string) error {
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
respBody, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
|
||||||
|
log.Printf("triggerPasswordReset: status=%d body=%s", resp.StatusCode, string(respBody))
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
return fmt.Errorf("authelia reset returned %d", resp.StatusCode)
|
return fmt.Errorf("authelia reset returned %d: %s", resp.StatusCode, string(respBody))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var result struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
}
|
||||||
|
if json.Unmarshal(respBody, &result) == nil && result.Status == "KO" {
|
||||||
|
return fmt.Errorf("authelia reset rejected: %s", string(respBody))
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,8 @@ func (a *App) handleLanding(w http.ResponseWriter, r *http.Request) {
|
||||||
tier := pricing.ForCustomer(count, a.cfg.FreeTierLimit, a.cfg.YearTierLimit)
|
tier := pricing.ForCustomer(count, a.cfg.FreeTierLimit, a.cfg.YearTierLimit)
|
||||||
|
|
||||||
useForm := a.cfg.StripePriceID != "" || a.cfg.StripePriceIDFree != "" ||
|
useForm := a.cfg.StripePriceID != "" || a.cfg.StripePriceIDFree != "" ||
|
||||||
a.cfg.StripePriceIDYear != "" || a.cfg.StripePriceIDMonth200 != ""
|
a.cfg.StripePriceIDYear != "" || a.cfg.StripePriceIDMonth100 != "" ||
|
||||||
|
a.cfg.StripePriceIDMonth200 != ""
|
||||||
data := map[string]any{
|
data := map[string]any{
|
||||||
"AppURL": a.cfg.AppURL,
|
"AppURL": a.cfg.AppURL,
|
||||||
"Commit": version.Commit,
|
"Commit": version.Commit,
|
||||||
|
|
@ -89,7 +90,7 @@ func (a *App) handleCreateCheckout(w http.ResponseWriter, r *http.Request) {
|
||||||
sess, err := a.stripe.CreateCheckoutSession(email, domain, phone, count)
|
sess, err := a.stripe.CreateCheckoutSession(email, domain, phone, count)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, stripe.ErrNoPriceForTier) {
|
if errors.Is(err, stripe.ErrNoPriceForTier) {
|
||||||
http.Error(w, "pricing not configured for current tier", http.StatusServiceUnavailable)
|
http.Error(w, "pricing not configured for current tier — set STRIPE_PRICE_ID or tier prices in env", http.StatusServiceUnavailable)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Printf("stripe checkout error: %v", err)
|
log.Printf("stripe checkout error: %v", err)
|
||||||
|
|
@ -152,7 +153,7 @@ func (a *App) handleSuccess(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
if result.IsNew || !inGroup {
|
if result.IsNew || !inGroup {
|
||||||
// New or lapsed: send password email, show success page.
|
// New or lapsed: send password email, show success page.
|
||||||
if err := a.triggerPasswordReset(result.Username); err != nil {
|
if err := a.triggerPasswordReset(r, result.Username); err != nil {
|
||||||
log.Printf("authelia reset trigger failed for %s: %v", username, err)
|
log.Printf("authelia reset trigger failed for %s: %v", username, err)
|
||||||
} else {
|
} else {
|
||||||
resendRateLimiter.record(result.Username)
|
resendRateLimiter.record(result.Username)
|
||||||
|
|
@ -213,7 +214,7 @@ func (a *App) handleResubscribe(w http.ResponseWriter, r *http.Request) {
|
||||||
sess, err := a.stripe.CreateCheckoutForCustomer(customerID, count)
|
sess, err := a.stripe.CreateCheckoutForCustomer(customerID, count)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, stripe.ErrNoPriceForTier) {
|
if errors.Is(err, stripe.ErrNoPriceForTier) {
|
||||||
http.Error(w, "pricing not configured for current tier", http.StatusServiceUnavailable)
|
http.Error(w, "pricing not configured for current tier — set STRIPE_PRICE_ID or tier prices in env", http.StatusServiceUnavailable)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Printf("stripe resubscribe error: %v", err)
|
log.Printf("stripe resubscribe error: %v", err)
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,7 @@ func (c *Client) ProvisionUser(username, email, stripeCustomerID, phone string)
|
||||||
if phone != "" {
|
if phone != "" {
|
||||||
_ = c.SetCustomerPhone(username, phone)
|
_ = c.SetCustomerPhone(username, phone)
|
||||||
}
|
}
|
||||||
|
_ = c.ensureDisplayName(conn, username, email)
|
||||||
return &ProvisionResult{Username: username, IsNew: false}, nil
|
return &ProvisionResult{Username: username, IsNew: false}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -70,6 +71,7 @@ func (c *Client) ProvisionUser(username, email, stripeCustomerID, phone string)
|
||||||
addReq.Attribute("sn", []string{username})
|
addReq.Attribute("sn", []string{username})
|
||||||
addReq.Attribute("uid", []string{username})
|
addReq.Attribute("uid", []string{username})
|
||||||
addReq.Attribute("mail", []string{email})
|
addReq.Attribute("mail", []string{email})
|
||||||
|
addReq.Attribute("displayName", []string{email})
|
||||||
if phone != "" {
|
if phone != "" {
|
||||||
addReq.Attribute("telephoneNumber", []string{phone})
|
addReq.Attribute("telephoneNumber", []string{phone})
|
||||||
}
|
}
|
||||||
|
|
@ -98,6 +100,18 @@ func (c *Client) ProvisionUser(username, email, stripeCustomerID, phone string)
|
||||||
return &ProvisionResult{Username: username, Password: password, IsNew: true}, nil
|
return &ProvisionResult{Username: username, Password: password, IsNew: true}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Client) ensureDisplayName(conn *goldap.Conn, username, email string) error {
|
||||||
|
userDN := fmt.Sprintf("uid=%s,ou=people,%s", username, c.cfg.LDAPBaseDN)
|
||||||
|
modReq := goldap.NewModifyRequest(userDN, nil)
|
||||||
|
modReq.Replace("displayName", []string{email})
|
||||||
|
if err := conn.Modify(modReq); err != nil {
|
||||||
|
log.Printf("ldap ensure displayName for %s: %v (may already be set)", username, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Printf("ldap set displayName for %s to %s", username, email)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Client) EnsureUser(username, email, stripeCustomerID, phone string) error {
|
func (c *Client) EnsureUser(username, email, stripeCustomerID, phone string) error {
|
||||||
_, err := c.ProvisionUser(username, email, stripeCustomerID, phone)
|
_, err := c.ProvisionUser(username, email, stripeCustomerID, phone)
|
||||||
return err
|
return err
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,9 @@ func (c *Client) priceForTier(t pricing.Tier) string {
|
||||||
if c.cfg.StripePriceIDMonth200 != "" {
|
if c.cfg.StripePriceIDMonth200 != "" {
|
||||||
return c.cfg.StripePriceIDMonth200
|
return c.cfg.StripePriceIDMonth200
|
||||||
}
|
}
|
||||||
|
if c.cfg.StripePriceIDMonth100 != "" {
|
||||||
|
return c.cfg.StripePriceIDMonth100
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return c.cfg.StripePriceID
|
return c.cfg.StripePriceID
|
||||||
}
|
}
|
||||||
|
|
@ -53,6 +56,8 @@ func (c *Client) CreateCheckoutSession(email, customerDomain, customerPhone stri
|
||||||
t := pricing.ForCustomer(customerCount, c.cfg.FreeTierLimit, c.cfg.YearTierLimit)
|
t := pricing.ForCustomer(customerCount, c.cfg.FreeTierLimit, c.cfg.YearTierLimit)
|
||||||
priceID := c.priceForTier(t)
|
priceID := c.priceForTier(t)
|
||||||
if priceID == "" {
|
if priceID == "" {
|
||||||
|
log.Printf("stripe: no price for tier %d (count=%d freeLimit=%d yearLimit=%d); set STRIPE_PRICE_ID or tier-specific price",
|
||||||
|
t, customerCount, c.cfg.FreeTierLimit, c.cfg.YearTierLimit)
|
||||||
return nil, ErrNoPriceForTier
|
return nil, ErrNoPriceForTier
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -81,6 +86,7 @@ func (c *Client) CreateCheckoutForCustomer(customerID string, customerCount int)
|
||||||
t := pricing.ForCustomer(customerCount, c.cfg.FreeTierLimit, c.cfg.YearTierLimit)
|
t := pricing.ForCustomer(customerCount, c.cfg.FreeTierLimit, c.cfg.YearTierLimit)
|
||||||
priceID := c.priceForTier(t)
|
priceID := c.priceForTier(t)
|
||||||
if priceID == "" {
|
if priceID == "" {
|
||||||
|
log.Printf("stripe: no price for tier %d (count=%d); set STRIPE_PRICE_ID or tier-specific price", t, customerCount)
|
||||||
return nil, ErrNoPriceForTier
|
return nil, ErrNoPriceForTier
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -140,15 +140,6 @@ services:
|
||||||
- "traefik.http.routers.authelia.entrypoints=websecure"
|
- "traefik.http.routers.authelia.entrypoints=websecure"
|
||||||
- "traefik.http.routers.authelia.tls=true"
|
- "traefik.http.routers.authelia.tls=true"
|
||||||
- "traefik.http.services.authelia.loadbalancer.server.port=9091"
|
- "traefik.http.services.authelia.loadbalancer.server.port=9091"
|
||||||
- "traefik.http.middlewares.fix-reset-url.redirectregex.regex=^/login\\?token=([^&]+)$$"
|
|
||||||
- "traefik.http.middlewares.fix-reset-url.redirectregex.replacement=https://bc.a250.ca/login/reset-password/step2?token=$${1}"
|
|
||||||
- "traefik.http.middlewares.fix-reset-url.redirectregex.permanent=false"
|
|
||||||
- "traefik.http.routers.authelia-reset.rule=Host(`bc.a250.ca`) && Path(`/login`) && QueryRegexp(`token`, `.+`)"
|
|
||||||
- "traefik.http.routers.authelia-reset.priority=20"
|
|
||||||
- "traefik.http.routers.authelia-reset.entrypoints=websecure"
|
|
||||||
- "traefik.http.routers.authelia-reset.tls=true"
|
|
||||||
- "traefik.http.routers.authelia-reset.middlewares=fix-reset-url@swarm"
|
|
||||||
- "traefik.http.routers.authelia-reset.service=authelia"
|
|
||||||
- "traefik.http.middlewares.authelia-auth.forwardauth.address=http://authelia:9091/login/api/authz/forward-auth?rd=https://bc.a250.ca/login/"
|
- "traefik.http.middlewares.authelia-auth.forwardauth.address=http://authelia:9091/login/api/authz/forward-auth?rd=https://bc.a250.ca/login/"
|
||||||
- "traefik.http.middlewares.authelia-auth.forwardauth.trustForwardHeader=true"
|
- "traefik.http.middlewares.authelia-auth.forwardauth.trustForwardHeader=true"
|
||||||
- "traefik.http.middlewares.authelia-auth.forwardauth.authResponseHeaders=Remote-User,Remote-Groups,Remote-Name,Remote-Email"
|
- "traefik.http.middlewares.authelia-auth.forwardauth.authResponseHeaders=Remote-User,Remote-Groups,Remote-Name,Remote-Email"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue