3
0

Add support for MFA

This commit is contained in:
Denis Arh
2021-03-04 15:18:41 +01:00
parent 94e3771960
commit cbc5034e8f
57 changed files with 3832 additions and 234 deletions

View File

@@ -5,7 +5,7 @@ import (
"crypto/tls"
"fmt"
authService "github.com/cortezaproject/corteza-server/auth"
"github.com/cortezaproject/corteza-server/auth/settings"
authSettings "github.com/cortezaproject/corteza-server/auth/settings"
cmpService "github.com/cortezaproject/corteza-server/compose/service"
cmpEvent "github.com/cortezaproject/corteza-server/compose/service/event"
fdrService "github.com/cortezaproject/corteza-server/federation/service"
@@ -428,7 +428,7 @@ func updateAuthSettings(svc authServicer, current *types.AppSettings) {
var (
// current auth settings
cas = current.Auth
providers []settings.Provider
providers []authSettings.Provider
)
for _, p := range cas.External.Providers {
@@ -436,7 +436,7 @@ func updateAuthSettings(svc authServicer, current *types.AppSettings) {
continue
}
providers = append(providers, settings.Provider{
providers = append(providers, authSettings.Provider{
Handle: p.Handle,
Label: p.Label,
IssuerUrl: p.IssuerUrl,
@@ -446,12 +446,20 @@ func updateAuthSettings(svc authServicer, current *types.AppSettings) {
})
}
svc.UpdateSettings(&settings.Settings{
as := &authSettings.Settings{
LocalEnabled: cas.Internal.Enabled,
SignupEnabled: cas.Internal.Signup.Enabled,
EmailConfirmationRequired: cas.Internal.Signup.EmailConfirmationRequired,
PasswordResetEnabled: cas.Internal.PasswordReset.Enabled,
ExternalEnabled: cas.External.Enabled,
Providers: providers,
})
}
as.MultiFactor.TOTP.Enabled = cas.MultiFactor.TOTP.Enabled
as.MultiFactor.TOTP.Enforced = cas.MultiFactor.TOTP.Enforced
as.MultiFactor.TOTP.Issuer = cas.MultiFactor.TOTP.Issuer
as.MultiFactor.EmailOTP.Enabled = cas.MultiFactor.EmailOTP.Enabled
as.MultiFactor.EmailOTP.Enforced = cas.MultiFactor.EmailOTP.Enforced
svc.UpdateSettings(as)
}

17
app/rand.go Normal file
View File

@@ -0,0 +1,17 @@
package app
import (
cr "crypto/rand"
"encoding/binary"
"math/rand"
)
func init() {
// seed some randomness
var b [8]byte
if _, err := cr.Read(b[:]); err != nil {
panic(err)
}
rand.Seed(int64(binary.LittleEndian.Uint64(b[:])))
}

View File

@@ -9,4 +9,6 @@ $(function () {
.attr('disabled', true)
}, 50)
})
$('input.mfa-code-mask').mask('000 000')
})

View File

@@ -18,5 +18,6 @@
</body>
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-Piv4xVNRyMGpqkS2by6br4gNJ7DXjqk09RmUpJ8jgGtD7zP9yug3goQfGII0yAns" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery.mask/1.14.16/jquery.mask.js" integrity="sha512-0XDfGxFliYJPFrideYOoxdgNIvrwGTLnmK20xZbCAvPfLGQMzHUsaqZK8ZoH+luXGRxTrS46+Aq400nCnAT0/w==" crossorigin="anonymous"></script>
<script src="{{ links.Assets }}/script.js?{{ buildtime }}"></script>
</html>

View File

@@ -12,6 +12,9 @@
<!-- Nunito font -->
<link rel="preconnect" href="https://fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;600&display=swap" rel="stylesheet">
<title>
Corteza
</title>
</head>
<body style="background: url({{ links.Assets }}/background.jpeg) no-repeat bottom;background-size: cover;background-attachment: fixed;" class="d-flex">
<main class="auth justify-content-center align-items-center">

View File

@@ -5,6 +5,8 @@
</a>
</div>
{{ $activeNav := default "" .activeNav }}
{{ if not .hideNav }}
{{ if and .user .client }}
<div class="py-1 px-3">
@@ -15,20 +17,18 @@
</div>
{{ else if .user }}
<ul class="nav ml-1 d-flex justify-content-around">
<li class="nav-item {{ if eq .activeNav "profile" }}active{{ end }}">
<li class="nav-item {{ if eq $activeNav "profile" }}active{{ end }}">
<a class="nav-link" href="{{ links.Profile }}">Your profile</a>
</li>
<li class="nav-item {{ if eq .activeNav "security" }}active{{ end }}">
<li class="nav-item {{ if eq $activeNav "security" }}active{{ end }}">
<a class="nav-link" href="{{ links.Security }}">Security</a>
</li>
<li class="nav-item {{ if eq .activeNav "sessions" }}active{{ end }}">
<li class="nav-item {{ if eq $activeNav "sessions" }}active{{ end }}">
<a class="nav-link" href="{{ links.Sessions }}">Login sessions</a>
</li>
{{ if .settings.ExternalEnabled }}
<li class="nav-item {{ if eq .activeNav "clients" }}active{{ end }}">
<li class="nav-item {{ if eq $activeNav "clients" }}active{{ end }}">
<a class="nav-link" href="{{ links.AuthorizedClients }}">Authorized clients</a>
</li>
{{ end }}
</ul>
{{ end }}
{{ end }}

View File

@@ -0,0 +1,43 @@
{{ template "inc_header.html.tpl" set . "hideNav" true }}
<div class="card-body p-0">
<h4 class="card-title p-3 border-bottom">Disable two-factor authentication with TOTP</h4>
<form
class="p-3"
method="POST"
action="{{ links.MfaTotpDisable }}"
>
Disable by entering existing code.
{{ if .form.error }}
<div class="alert alert-danger" role="alert">
{{ .form.error }}
</div>
{{ end }}
{{ .csrfField }}
<div class="input-group my-3">
<input
type="text"
required
class="form-control lg text-center mfa-code-mask"
name="code"
maxlength="6"
minlength="6"
aria-required="true"
placeholder="000 000"
autocomplete="off"
aria-label="Code">
</div>
<button
class="btn btn-primary btn-block btn-lg"
name="keep-session"
value="true"
type="submit"
>
Remove
</button>
</form>
</div>
{{ template "inc_footer.html.tpl" . }}

View File

@@ -0,0 +1,89 @@
{{ template "inc_header.html.tpl" set . "hideNav" true }}
<div class="card-body p-0">
<h4 class="card-title p-3 border-bottom">Configure two-factor authentication with TOTP</h4>
{{ if .enforced }}
<p class="p-3 text-danger">
TOTP multi factor authentication is enforced by Corteza administrator.
Please configure it right away.
</p>
{{ end }}
<div class="container p-3 m-0">
<div class="row">
<div class="col-6 p-0">
<pre class="h5 px-4">{{ .secret }}</pre>
<img style="width: 280px" src="{{ links.MfaTotpQRImage }}" />
<form
class="px-3"
method="POST"
action="{{ links.MfaTotpNewSecret }}"
>
Complete the configuration by entering code from the authenticator application:
{{ if .form.error }}
<div class="alert alert-danger" role="alert">
{{ .form.error }}
</div>
{{ end }}
{{ .csrfField }}
<div class="input-group my-3">
<input
type="text"
required
class="form-control lg text-center mfa-code-mask"
name="code"
maxlength="6"
minlength="6"
aria-required="true"
placeholder="000 000"
autocomplete="off"
aria-label="Code">
</div>
<button
class="btn btn-primary btn-block btn-lg"
name="keep-session"
value="true"
type="submit"
>
Submit
</button>
</form>
</div>
<div class="col-6 p-0">
<p>
Corteza uses time based one time passwords (TOTP) as one of the
underlying technologies for two-factor authentication.
Use one of the applications listed below and type in the secret or scan the QR code.
</p>
<p>
This will enable additional security for your account.
</p>
<p>
You can use one of the following applications:
</p>
<ul>
<li>
<a target="_blank" href="https://lastpass.com/auth/">LastPass Authenticator</a>
</li>
<li>
Google Authenticator in <br />
<a target="_blank" href="https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2">Android</a>
or
<a target="_blank" href="https://apps.apple.com/us/app/google-authenticator/id388497605">App Store</a>
</li>
<li>
<a target="_blank" href="https://authy.com">Authy</a>
</li>
</ul>
</div>
</div>
</div>
</div>
{{ template "inc_footer.html.tpl" . }}

View File

@@ -0,0 +1,104 @@
{{ template "inc_header.html.tpl" set . "hideNav" true }}
<div class="card-body p-0">
<h4 class="card-title p-3 border-bottom">Multi-factor authentication</h4>
{{ if .emailOtpPending }}
<form
class="p-3"
method="POST"
action="{{ links.Mfa }}"
>
<h5>Check your inbox and enter the received code</h5>
{{ if .form.emailOtpError }}
<div class="alert alert-danger" role="alert">
{{ .form.emailOtpError }}
</div>
{{ end }}
{{ .csrfField }}
<div class="input-group my-3">
<input
type="text"
required
class="form-control text-center mfa-code-mask"
name="code"
maxlength="6"
minlength="6"
aria-required="true"
placeholder="000 000"
autocomplete="off"
aria-label="Code">
</div>
<button
class="btn btn-primary btn-block btn-lg"
name="action"
value="verifyEmailOtp"
type="submit"
>
Verify
</button>
<a
href="{{ links.Mfa }}?action=resendEmailOtp"
class="btn btn-link btn-block btn-lg"
name="action"
value="resendEmailOtp"
>
Resend
</a>
</form>
{{ else if not .emailOtpDisabled }}
<p class="p-3">
<i class="bi bi-check text-primary"></i> Email OTP confirmed
</p>
{{ end }}
{{ if .totpPending }}
<form
class="p-3"
method="POST"
action="{{ links.Mfa }}"
>
<h5>Check your TOTP application and enter the received code</h5>
{{ if .form.totpError }}
<div class="alert alert-danger" role="alert">
{{ .form.totpError }}
</div>
{{ end }}
{{ .csrfField }}
<div class="input-group my-3">
<input
type="text"
required
class="form-control text-center mfa-code-mask"
name="code"
maxlength="6"
minlength="6"
aria-required="true"
placeholder="000 000"
autocomplete="off"
aria-label="Code">
</div>
<button
class="btn btn-primary btn-block btn-lg"
type="submit"
name="action"
value="verifyTotp"
>
Verify
</button>
</form>
{{ else if not .totpDisabled }}
<p class="p-3">
<i class="bi bi-check text-primary"></i> TOTP confirmed
</p>
{{ end }}
</div>
{{ template "inc_footer.html.tpl" . }}

View File

@@ -52,7 +52,7 @@
<label for="profileFormHandle">Handle</label>
<input
type="text"
class="form-control"
class="form-control handle-mask"
name="handle"
id="profileFormHandle"
placeholder="Short name, nickname or handle"

View File

@@ -47,6 +47,29 @@ login:
email: some.email@example.tld
error: "There was an error..."
security:
Default:
user: { ID: 123, Name: John Doe }
settings:
LocalEnabled: true
MFA enabled:
user: { ID: 123, Name: John Doe }
settings:
LocalEnabled: true
MultiFactor:
TOTP: { Enabled: true }
EmailOTP: { Enabled: true }
MFA enforced by user:
user: { ID: 123, Name: John Doe }
totpEnforced: false
emailOtpEnforced: true
settings:
LocalEnabled: true
MultiFactor:
TOTP: { Enabled: true }
EmailOTP: { Enabled: true }
logout:
Default: {}
With error:
@@ -115,6 +138,7 @@ profile:
client:
Name: foo
emailConfirmationRequired: true
change-password:
Default: {}
With error:

View File

@@ -1,10 +1,92 @@
{{ template "inc_header.html.tpl" set . "activeNav" "security" }}
<div class="card-body p-0">
<h4 class="card-title p-3 border-bottom">Security</h4>
<ul>
{{ if .settings.LocalEnabled }}
<li><a href="{{ links.ChangePassword }}">Change your password</a></li>
<form
method="POST"
action="{{ links.Security }}"
class="p-3"
>
{{ if .settings.LocalEnabled }}
<h5>Password</h5>
<a href="{{ links.ChangePassword }}">Change your password</a>
{{ end }}
<hr />
<div>
{{ .csrfField }}
<h5>Multi-factor authentication</h5>
{{ if or .settings.MultiFactor.TOTP.Enabled .settings.MultiFactor.EmailOTP.Enabled }}
{{ if .settings.MultiFactor.TOTP.Enabled }}
<div class="py-4">
<h6>Additional security with mobile app (time-based one-time-password)</h6>
<div class="row">
<div class="col-1 text-left">
{{ if .totpEnforced }}
<h4><i class="bi bi-check-square text-primary"></i></h4>
{{ else }}
<h4><i class="bi bi-exclamation-triangle-fill text-danger"></i></h4>
{{ end }}
</div>
<div class="col-7 pt-2">
{{ if .totpEnforced }}
Configured and required on login.
{{ else }}
Currently disabled.
{{ end }}
</div>
<div class="col-5">
{{ if .totpEnforced }}
{{ if not .settings.MultiFactor.TOTP.Enforced }}
<button name="action" value="disableTOTP" class="btn btn-link text-danger">Disable</button>
{{ end }}
{{ else }}
<button name="action" value="configureTOTP" class="btn btn-link text-primary">Configure</button>
{{ end }}
</div>
</div>
</div>
{{ end }}
{{ if .settings.MultiFactor.EmailOTP.Enabled }}
<div class="py-4">
<h6>Additional security with one-time-password over email</h6>
<div class="row">
<div class="col-1 text-left">
{{ if .emailOtpEnforced }}
<h4><i class="bi bi-check-square text-primary"></i></h4>
{{ else }}
<h4><i class="bi bi-exclamation-triangle-fill text-danger"></i></h4>
{{ end }}
</div>
<div class="col-7 pt-2">
{{ if .emailOtpEnforced }}
Enabled and required on login.
{{ else }}
Currently disabled.
{{ end }}
</div>
<div class="col-4">
{{ if .emailOtpEnforced }}
{{ if not .settings.MultiFactor.EmailOTP.Enforced }}
<button name="action" value="disableEmailOTP" class="btn btn-link text-danger">Disable</button>
{{ end }}
{{ else }}
<button name="action" value="enableEmailOTP" class="btn btn-link text-primary">Enable</button>
{{ end }}
</div>
</div>
</div>
{{ end }}
{{ else }}
<div class="text-danger font-weight-bold mb-4" role="alert">
All MFA methods are currently disabled. Ask your administrator to enable them.
</div>
{{ end }}
</ul>
</div>
</form>
</div>
{{ template "inc_footer.html.tpl" . }}

View File

@@ -59,7 +59,7 @@
</label>
<input
type="text"
class="form-control"
class="form-control handle-mask"
name="handle"
placeholder="Short name, nickname or handle"
value="{{ .form.handle }}"

View File

@@ -8,7 +8,7 @@ import (
"github.com/cortezaproject/corteza-server/auth/external"
"github.com/cortezaproject/corteza-server/auth/handlers"
"github.com/cortezaproject/corteza-server/auth/oauth2"
"github.com/cortezaproject/corteza-server/auth/session"
"github.com/cortezaproject/corteza-server/auth/request"
"github.com/cortezaproject/corteza-server/auth/settings"
"github.com/cortezaproject/corteza-server/pkg/actionlog"
"github.com/cortezaproject/corteza-server/pkg/auth"
@@ -64,7 +64,7 @@ func New(ctx context.Context, log *zap.Logger, s store.Storer, opt options.AuthO
log = zap.NewNop()
}
sesManager := session.NewManager(s, opt, log)
sesManager := request.NewSessionManager(s, opt, log)
oauth2Manager := oauth2.NewManager(
opt,
@@ -232,6 +232,10 @@ func (svc *service) UpdateSettings(s *settings.Settings) {
svc.log.Debug("setting changed", zap.Bool("externalEnabled", s.ExternalEnabled))
}
if svc.settings.MultiFactor != s.MultiFactor {
svc.log.Debug("setting changed", zap.Any("mfa", s.MultiFactor))
}
if len(svc.settings.Providers) != len(s.Providers) {
svc.log.Debug("setting changed", zap.Int("providers", len(s.Providers)))
external.SetupGothProviders(svc.opt.ExternalRedirectURL, s.Providers...)

View File

@@ -2,6 +2,7 @@ package handlers
import (
"github.com/cortezaproject/corteza-server/auth/request"
"github.com/cortezaproject/corteza-server/pkg/errors"
"github.com/cortezaproject/corteza-server/system/types"
"sort"
"strconv"
@@ -54,7 +55,7 @@ func (h *AuthHandlers) clientsProc(req *request.AuthReq) error {
return err
}
if err = h.ClientService.Revoke(req.Context(), req.User.ID, clientID); err != nil {
if err = h.ClientService.Revoke(req.Context(), req.AuthUser.User.ID, clientID); err != nil {
return err
}
@@ -73,28 +74,33 @@ func (h *AuthHandlers) getAuthorizedClients(req *request.AuthReq) (ss authClient
set types.AuthConfirmedClientSet
client *types.AuthClient
)
if set, err = h.ClientService.Confirmed(req.Context(), req.User.ID); err != nil {
if set, err = h.ClientService.Confirmed(req.Context(), req.AuthUser.User.ID); err != nil {
return
}
ss = make(authClients, len(set))
ss = make(authClients, 0, len(set))
for i := range set {
client, err = h.ClientService.LookupByID(req.Context(), set[i].ClientID)
if errors.IsNotFound(err) {
continue
}
if err != nil {
return
}
ss[i] = authClient{
ac := authClient{
ID: set[i].ClientID,
Name: client.Handle,
ConfirmedAt: set[i].ConfirmedAt,
}
if client.Meta != nil {
ss[i].Name = client.Meta.Name
ac.Name = client.Meta.Name
}
ss = append(ss, ac)
}
return

View File

@@ -2,6 +2,7 @@ package handlers
import (
"github.com/cortezaproject/corteza-server/auth/request"
"github.com/cortezaproject/corteza-server/pkg/auth"
"github.com/cortezaproject/corteza-server/system/service"
"go.uber.org/zap"
)
@@ -15,8 +16,8 @@ func (h *AuthHandlers) changePasswordForm(req *request.AuthReq) error {
func (h *AuthHandlers) changePasswordProc(req *request.AuthReq) (err error) {
err = h.AuthService.ChangePassword(
req.Context(),
req.User.ID,
auth.SetIdentityToContext(req.Context(), req.AuthUser.User),
req.AuthUser.User.ID,
req.Request.PostFormValue("oldPassword"),
req.Request.PostFormValue("newPassword"),
)

View File

@@ -4,7 +4,6 @@ import (
"context"
"fmt"
"github.com/cortezaproject/corteza-server/auth/request"
"github.com/cortezaproject/corteza-server/auth/session"
"github.com/cortezaproject/corteza-server/pkg/api"
"github.com/cortezaproject/corteza-server/system/types"
"github.com/go-chi/chi"
@@ -43,23 +42,29 @@ func (h AuthHandlers) externalCallback(w http.ResponseWriter, r *http.Request) {
// to a current user
func (h AuthHandlers) handleSuccessfulExternalAuth(w http.ResponseWriter, r *http.Request, cred goth.User) {
var (
authUser *types.User
err error
ctx = r.Context()
user *types.User
err error
ctx = r.Context()
)
h.Log.Info("login successful", zap.String("provider", cred.Provider))
// Try to login/sign-up external user
if authUser, err = h.AuthService.External(ctx, cred); err != nil {
if user, err = h.AuthService.External(ctx, cred); err != nil {
api.Send(w, r, err)
return
}
h.handle(func(req *request.AuthReq) error {
h.storeUserToSession(req, authUser)
req.AuthUser = request.NewAuthUser(h.Settings, user, false, 0)
if session.GetOAuth2AuthParams(req.Session) != nil {
// auto-complete EmailOTP and TOTP when authenticating via external identity provider
req.AuthUser.CompleteEmailOTP()
req.AuthUser.CompleteTOTP()
req.AuthUser.Save(req.Session)
if request.GetOAuth2AuthParams(req.Session) != nil {
// If we have oauth2 auth params stored in the session,
// try and continue with the oauth2 flow
req.RedirectTo = GetLinks().OAuth2Authorize

View File

@@ -2,7 +2,8 @@ package handlers
import (
"github.com/cortezaproject/corteza-server/auth/request"
"github.com/cortezaproject/corteza-server/auth/session"
"github.com/cortezaproject/corteza-server/pkg/auth"
"github.com/cortezaproject/corteza-server/pkg/errors"
"github.com/cortezaproject/corteza-server/system/service"
"github.com/cortezaproject/corteza-server/system/types"
"go.uber.org/zap"
@@ -25,37 +26,49 @@ func (h *AuthHandlers) loginProc(req *request.AuthReq) (err error) {
req.SetKV(nil)
var (
authUser *types.User
email = req.Request.PostFormValue("email")
user *types.User
email = req.Request.PostFormValue("email")
)
authUser, err = h.AuthService.InternalLogin(
user, err = h.AuthService.InternalLogin(
req.Context(),
email,
req.Request.PostFormValue("password"),
)
if err == nil {
req.NewAlerts = append(req.NewAlerts, request.Alert{
Type: "primary",
Text: "You are now logged-in",
})
var (
isPerm = len(req.Request.PostFormValue("keep-session")) > 0
lifetime = h.Opt.SessionLifetime
)
if session.GetOAuth2AuthParams(req.Session) == nil {
// Not in the OAuth2 flow, go to profile
req.RedirectTo = GetLinks().Profile
} else {
h.Log.Info("oauth2 params found, continuing with authorization flow")
req.RedirectTo = GetLinks().OAuth2AuthorizeClient
if isPerm {
lifetime = h.Opt.SessionPermLifetime
}
h.Log.Info("login successful")
h.storeUserToSession(req, authUser)
req.AuthUser = request.NewAuthUser(h.Settings, user, isPerm, lifetime)
if len(req.Request.PostFormValue("keep-session")) > 0 {
session.SetPerm(req.Session, h.Opt.SessionPermLifetime)
req.AuthUser.Save(req.Session)
h.Log.Info(
"login with password successful",
zap.Any("mfa", req.AuthUser.MFAStatus),
zap.Bool("perm-login", isPerm),
zap.Duration("lifetime", lifetime),
)
req.PushAlert("You are now logged-in")
if req.AuthUser.PendingEmailOTP() {
// Email OTP enforced (globally or by user sec. policy)
//
// @todo this should probably be part of login in auth service?
if err = h.AuthService.SendEmailOTP(auth.SetIdentityToContext(req.Context(), req.AuthUser.User)); err != nil {
return errors.Internal("could not send OTP via email, contact your administrator").Wrap(err)
}
}
handleSuccessfulAuth(req)
return nil
}
@@ -92,14 +105,11 @@ func (h *AuthHandlers) onlyIfLocalEnabled(fn handlerFn) handlerFn {
}
func (h *AuthHandlers) localDisabledAlert(req *request.AuthReq) {
if req.User != nil {
if req.AuthUser.User != nil {
req.RedirectTo = GetLinks().Profile
} else {
req.RedirectTo = GetLinks().Login
}
req.NewAlerts = append(req.NewAlerts, request.Alert{
Type: "danger",
Text: "Local accounts disabled",
})
req.PushDangerAlert("Local accounts disabled")
}

View File

@@ -16,12 +16,11 @@ func (h *AuthHandlers) logoutProc(req *request.AuthReq) (err error) {
}
// Prevent these two to be rendered by in the template
req.User = nil
req.AuthUser = nil
req.Client = nil
h.Log.Info("logout successful")
req.Template = TmplLogout
req.Data["backlink"] = req.Request.FormValue("back")
return
}

View File

@@ -0,0 +1,80 @@
package handlers
import (
"github.com/cortezaproject/corteza-server/auth/request"
"github.com/cortezaproject/corteza-server/pkg/auth"
)
// Handles MFA TOTP configuration form
//
// Where the TOTP QR & code are displayed and where
func (h AuthHandlers) mfaForm(req *request.AuthReq) (err error) {
req.Template = TmplMfa
if !req.AuthUser.PendingMFA() {
req.RedirectTo = GetLinks().Profile
return nil
}
if req.Request.URL.Query().Get("action") == "resendEmailOtp" {
err = h.AuthService.SendEmailOTP(
auth.SetIdentityToContext(req.Context(), req.AuthUser.User),
)
if err != nil {
req.SetKV(map[string]string{"emailOtpError": err.Error()})
return nil
}
req.PushAlert("Email OTP resent")
req.RedirectTo = GetLinks().Mfa
}
req.Data["form"] = req.GetKV()
req.Data["emailOtpDisabled"] = req.AuthUser.DisabledEmailOTP()
req.Data["emailOtpPending"] = req.AuthUser.PendingEmailOTP()
req.Data["totpDisabled"] = req.AuthUser.DisabledTOTP()
req.Data["totpPending"] = req.AuthUser.PendingTOTP()
return nil
}
// Handles MFA OTP form processing
func (h AuthHandlers) mfaProc(req *request.AuthReq) (err error) {
req.RedirectTo = GetLinks().Mfa
req.SetKV(nil)
switch req.Request.Form.Get("action") {
case "verifyEmailOtp":
err = h.AuthService.ValidateEmailOTP(
auth.SetIdentityToContext(req.Context(), req.AuthUser.User),
req.Request.PostFormValue("code"),
)
if err != nil {
req.SetKV(map[string]string{"emailOtpError": err.Error()})
return nil
}
req.PushAlert("Email OTP valid")
req.AuthUser.CompleteEmailOTP()
case "verifyTotp":
err = h.AuthService.ValidateTOTP(
auth.SetIdentityToContext(req.Context(), req.AuthUser.User),
req.Request.PostFormValue("code"),
)
if err != nil {
req.SetKV(map[string]string{"totpError": err.Error()})
return nil
}
req.PushAlert("TOTP valid")
req.AuthUser.CompleteTOTP()
}
// All required MFA's confirmed, proceed to profile
handleSuccessfulAuth(req)
return nil
}

View File

@@ -0,0 +1,209 @@
package handlers
import (
"encoding/base32"
"fmt"
"github.com/cortezaproject/corteza-server/auth/request"
"github.com/cortezaproject/corteza-server/pkg/auth"
"github.com/cortezaproject/corteza-server/pkg/errors"
"github.com/cortezaproject/corteza-server/system/types"
"go.uber.org/zap"
"math/rand"
"net/url"
"rsc.io/qr"
)
const (
// session key where the secret is kept between requests
totpSecretKey = "totpSecret"
)
// Handles MFA TOTP configuration form
//
// Where the TOTP QR & code are displayed and where
func (h AuthHandlers) mfaTotpConfigForm(req *request.AuthReq) (err error) {
var (
rawSecret [10]byte
secret string
// this is more for debugging & development purposes
// but it does not hurt to keep it here
_, fresh = req.Request.URL.Query()["fresh"]
)
if s, has := req.Session.Values[totpSecretKey]; has && !fresh {
// secret is already in the session and
// there's no explicit request to change it
secret = s.(string)
} else {
rand.Read(rawSecret[:])
secret = base32.StdEncoding.EncodeToString(rawSecret[:])
req.Session.Values[totpSecretKey] = secret
}
req.Data["secret"] = secret
req.Data["enforced"] = h.Settings.MultiFactor.TOTP.Enforced
req.Data["form"] = req.GetKV()
req.Template = TmplMfaTotp
req.SetKV(nil)
return nil
}
// Handles MFA OTP form processing
func (h AuthHandlers) mfaTotpConfigProc(req *request.AuthReq) (err error) {
req.RedirectTo = GetLinks().MfaTotpNewSecret
req.SetKV(nil)
var (
user *types.User
secret, has = req.Session.Values[totpSecretKey]
)
if !has {
return fmt.Errorf("no TOTP secret in session")
}
// Here is where code validation is done and where the secret is stored
user, err = h.AuthService.ConfigureTOTP(
auth.SetIdentityToContext(req.Context(), req.AuthUser.User),
secret.(string),
req.Request.PostFormValue("code"),
)
if err == nil {
req.NewAlerts = append(req.NewAlerts, request.Alert{
Type: "primary",
Text: "Two factor authentication with TOTP enabled",
})
// Make sure we update User's data in the session
req.AuthUser.User = user
req.AuthUser.CompleteTOTP()
req.AuthUser.Save(req.Session)
h.Log.Info("TOTP code verified")
req.RedirectTo = GetLinks().Security
delete(req.Session.Values, totpSecretKey)
return nil
}
switch {
case errors.IsInvalidData(err):
req.SetKV(map[string]string{
"error": "Invalid code format",
})
return nil
case errors.IsUnauthenticated(err):
req.SetKV(map[string]string{
"error": "Invalid code",
})
return nil
default:
// Just in case, delete secret if something unexpected happend
delete(req.Session.Values, totpSecretKey)
h.Log.Error("unhandled error", zap.Error(err))
return err
}
}
// Displays the QR PNG image
func (h AuthHandlers) mfaTotpConfigQR(req *request.AuthReq) (err error) {
var (
issuer = h.Settings.MultiFactor.TOTP.Issuer
secret, has = req.Session.Values[totpSecretKey]
)
if !has {
return fmt.Errorf("no secret in session")
}
if len(issuer) == 0 {
issuer = "Corteza"
}
account := req.AuthUser.User.Handle
if len(account) == 0 {
account = req.AuthUser.User.Email
}
URL, err := url.Parse("otpauth://totp")
if err != nil {
panic(err)
}
URL.Path += "/" + url.PathEscape(issuer) + ":" + url.PathEscape(account)
params := url.Values{}
params.Add("secret", secret.(string))
params.Add("issuer", issuer)
URL.RawQuery = params.Encode()
code, err := qr.Encode(URL.String(), qr.Q)
if err != nil {
panic(err)
}
req.Status = -1
_, err = req.Response.Write(code.PNG())
return
}
// Handles MFA TOTP configuration form
//
// Where the TOTP QR & code are displayed and where
func (h AuthHandlers) mfaTotpDisableForm(req *request.AuthReq) (err error) {
req.Data["form"] = req.GetKV()
req.Template = TmplMfaTotpDisable
req.SetKV(nil)
return nil
}
// Handles MFA OTP form processing
func (h AuthHandlers) mfaTotpDisableProc(req *request.AuthReq) (err error) {
req.RedirectTo = GetLinks().MfaTotpDisable
req.SetKV(nil)
var user *types.User
// Here is where code validation is done and where the secret is stored
user, err = h.AuthService.RemoveTOTP(
req.Context(),
req.AuthUser.User.ID,
req.Request.PostFormValue("code"),
)
if err == nil {
req.NewAlerts = append(req.NewAlerts, request.Alert{
Type: "primary",
Text: "Two factor authentication with TOTP disabled",
})
// Make sure we update User's data in the session
req.AuthUser.User = user
req.AuthUser.ResetTOTP()
req.AuthUser.Save(req.Session)
h.Log.Info("TOTP disabled")
req.RedirectTo = GetLinks().Security
return nil
}
switch {
case errors.IsInvalidData(err):
req.SetKV(map[string]string{
"error": "Invalid code format",
})
return nil
case errors.IsUnauthenticated(err):
req.SetKV(map[string]string{
"error": "Invalid code",
})
return nil
default:
h.Log.Error("unhandled error", zap.Error(err))
return err
}
}

View File

@@ -6,7 +6,6 @@ import (
"fmt"
"github.com/cortezaproject/corteza-server/auth/oauth2"
"github.com/cortezaproject/corteza-server/auth/request"
"github.com/cortezaproject/corteza-server/auth/session"
"github.com/cortezaproject/corteza-server/pkg/auth"
"github.com/cortezaproject/corteza-server/pkg/errors"
systemService "github.com/cortezaproject/corteza-server/system/service"
@@ -26,7 +25,7 @@ import (
// OA2 server internals first run user check (see SetUserAuthorizationHandler lambda)
// to ensure user is authenticated;
func (h AuthHandlers) oauth2Authorize(req *request.AuthReq) (err error) {
if form := session.GetOAuth2AuthParams(req.Session); form != nil {
if form := request.GetOAuth2AuthParams(req.Session); form != nil {
req.Request.Form = form
h.Log.Debug("restarting oauth2 authorization flow", zap.Any("params", req.Request.Form))
} else {
@@ -34,7 +33,7 @@ func (h AuthHandlers) oauth2Authorize(req *request.AuthReq) (err error) {
}
session.SetOauth2AuthParams(req.Session, nil)
request.SetOauth2AuthParams(req.Session, nil)
var (
ctx context.Context
@@ -53,7 +52,7 @@ func (h AuthHandlers) oauth2Authorize(req *request.AuthReq) (err error) {
if client != nil {
// No client validation is done at this point;
// first, see if user is able to authenticate.
session.SetOauth2Client(req.Session, client)
request.SetOauth2Client(req.Session, client)
}
// set to -1 to make sure that wrapping request handler
@@ -74,7 +73,7 @@ func (h AuthHandlers) oauth2Authorize(req *request.AuthReq) (err error) {
func (h AuthHandlers) oauth2AuthorizeClient(req *request.AuthReq) (err error) {
var (
client = session.GetOauth2Client(req.Session)
client = request.GetOauth2Client(req.Session)
)
if client == nil {
@@ -87,8 +86,8 @@ func (h AuthHandlers) oauth2AuthorizeClient(req *request.AuthReq) (err error) {
if !h.canAuthorizeClient(req.Context(), req.Client) {
h.Log.Error("user's roles do not allow authorization of this client", zap.Uint64("ID", req.Client.ID), zap.String("handle", req.Client.Handle))
session.SetOauth2Client(req.Session, nil)
session.SetOauth2AuthParams(req.Session, nil)
request.SetOauth2Client(req.Session, nil)
request.SetOauth2AuthParams(req.Session, nil)
req.RedirectTo = GetLinks().Profile
req.NewAlerts = append(req.NewAlerts, request.Alert{
Type: "danger",
@@ -97,7 +96,7 @@ func (h AuthHandlers) oauth2AuthorizeClient(req *request.AuthReq) (err error) {
return nil
}
if !req.User.EmailConfirmed {
if !req.AuthUser.User.EmailConfirmed {
req.Data["invalidUser"] = template.HTML(fmt.Sprintf(
`Can not continue with unauthorized email,
visit <a href="%s">your profile</a> and resolve the issue.`,
@@ -110,7 +109,7 @@ func (h AuthHandlers) oauth2AuthorizeClient(req *request.AuthReq) (err error) {
// Client is trusted, no need to show this screen
// move forward and authorize oauth2 request
session.SetOauth2ClientAuthorized(req.Session, true)
request.SetOauth2ClientAuthorized(req.Session, true)
req.RedirectTo = GetLinks().OAuth2Authorize
return nil
}
@@ -125,9 +124,9 @@ func (h AuthHandlers) oauth2AuthorizeClientProc(req *request.AuthReq) (err error
// permissions are already check ed in the oauth2AuthorizeClient fn,
// just making sure
if h.canAuthorizeClient(req.Context(), req.Client) {
session.SetOauth2Client(req.Session, nil)
request.SetOauth2Client(req.Session, nil)
if _, allow := req.Request.Form["allow"]; allow {
session.SetOauth2ClientAuthorized(req.Session, true)
request.SetOauth2ClientAuthorized(req.Session, true)
req.RedirectTo = GetLinks().OAuth2Authorize
return
}
@@ -138,7 +137,7 @@ func (h AuthHandlers) oauth2AuthorizeClientProc(req *request.AuthReq) (err error
// This occurs when user pressed "DENY" button on authorize-client form
// Remove all and redirect to profile
//
session.SetOauth2AuthParams(req.Session, nil)
request.SetOauth2AuthParams(req.Session, nil)
req.RedirectTo = GetLinks().Profile
req.NewAlerts = append(req.NewAlerts, request.Alert{
Type: "warning",
@@ -154,7 +153,7 @@ func (h AuthHandlers) canAuthorizeClient(ctx context.Context, c *types.AuthClien
func (h AuthHandlers) oauth2Token(req *request.AuthReq) (err error) {
// Cleanup
session.SetOauth2ClientAuthorized(req.Session, false)
request.SetOauth2ClientAuthorized(req.Session, false)
req.Status = -1
@@ -318,7 +317,7 @@ func (h AuthHandlers) loadRequestedClient(req *request.AuthReq) (client *types.A
return errors.InvalidData("invalid client ID")
}
if client = session.GetOauth2Client(req.Session); client != nil {
if client = request.GetOauth2Client(req.Session); client != nil {
h.Log.Debug("client loaded from session", zap.Uint64("ID", client.ID))
// ensure that session holds the right client and

View File

@@ -46,15 +46,15 @@ func (h *AuthHandlers) resetPasswordForm(req *request.AuthReq) (err error) {
req.Template = TmplResetPassword
if req.User == nil {
if req.AuthUser.User == nil {
// user not set, expecting valid token in URL
if token := req.Request.URL.Query().Get("token"); len(token) > 0 {
req.User, err = h.AuthService.ValidatePasswordResetToken(req.Context(), token)
req.AuthUser.User, err = h.AuthService.ValidatePasswordResetToken(req.Context(), token)
if err == nil {
// redirect back to self (but without token and with user in session
h.Log.Debug("valid password reset token found, refreshing page with stored user")
req.RedirectTo = GetLinks().ResetPassword
h.storeUserToSession(req, req.User)
req.AuthUser.Save(req.Session)
return nil
}
}
@@ -74,7 +74,7 @@ func (h *AuthHandlers) resetPasswordForm(req *request.AuthReq) (err error) {
func (h *AuthHandlers) resetPasswordProc(req *request.AuthReq) (err error) {
h.Log.Debug("password reset proc")
err = h.AuthService.SetPassword(req.Context(), req.User.ID, req.Request.PostFormValue("password"))
err = h.AuthService.SetPassword(req.Context(), req.AuthUser.User.ID, req.Request.PostFormValue("password"))
if err == nil {
req.NewAlerts = append(req.NewAlerts, request.Alert{

View File

@@ -8,34 +8,39 @@ import (
func (h *AuthHandlers) profileForm(req *request.AuthReq) error {
req.Template = TmplProfile
u := req.AuthUser.User
if form := req.GetKV(); len(form) > 0 {
req.Data["form"] = form
req.SetKV(nil)
} else {
req.Data["form"] = map[string]string{
"email": req.User.Email,
"handle": req.User.Handle,
"name": req.User.Name,
"email": u.Email,
"handle": u.Handle,
"name": u.Name,
}
}
req.Data["emailConfirmationRequired"] = !req.User.EmailConfirmed && h.Settings.EmailConfirmationRequired
req.Data["emailConfirmationRequired"] = !u.EmailConfirmed && h.Settings.EmailConfirmationRequired
return nil
}
func (h *AuthHandlers) profileProc(req *request.AuthReq) error {
req.RedirectTo = GetLinks().Profile
req.SetKV(nil)
u := req.AuthUser.User
req.User.Handle = req.Request.PostFormValue("handle")
req.User.Name = req.Request.PostFormValue("name")
u.Handle = req.Request.PostFormValue("handle")
u.Name = req.Request.PostFormValue("name")
// a little workaround to inject current user as authenticated identity into context
// this way user service will pass us through.
user, err := h.UserService.Update(req.Context(), req.User)
user, err := h.UserService.Update(req.Context(), u)
if err == nil {
h.storeUserToSession(req, user)
req.AuthUser.User = user
req.AuthUser.Save(req.Session)
req.NewAlerts = append(req.NewAlerts, request.Alert{
Type: "primary",
Text: "Profile successfully updated.",
@@ -54,9 +59,9 @@ func (h *AuthHandlers) profileProc(req *request.AuthReq) error {
service.UserErrNotAllowedToUpdate().Is(err):
req.SetKV(map[string]string{
"error": err.Error(),
"email": req.User.Email,
"handle": req.User.Handle,
"name": req.User.Name,
"email": u.Email,
"handle": u.Handle,
"name": u.Name,
})
req.NewAlerts = append(req.NewAlerts, request.Alert{

View File

@@ -2,9 +2,51 @@ package handlers
import (
"github.com/cortezaproject/corteza-server/auth/request"
"go.uber.org/zap"
)
func (h *AuthHandlers) security(req *request.AuthReq) error {
func (h *AuthHandlers) securityForm(req *request.AuthReq) error {
req.Template = TmplSecurity
// user's MFA security policy
umsp := req.AuthUser.User.Meta.SecurityPolicy.MFA
req.Data["emailOtpEnforced"] = umsp.EnforcedEmailOTP
req.Data["totpEnforced"] = umsp.EnforcedTOTP
return nil
}
func (h *AuthHandlers) securityProc(req *request.AuthReq) error {
req.RedirectTo = GetLinks().Security
action := req.Request.Form.Get("action")
switch action {
case "reconfigureTOTP", "configureTOTP":
// make sure secret is regenerated
delete(req.Session.Values, totpSecretKey)
req.RedirectTo = GetLinks().MfaTotpNewSecret
case "disableTOTP":
req.RedirectTo = GetLinks().MfaTotpDisable
case "disableEmailOTP", "enableEmailOTP":
enable := action == "enableEmailOTP"
if user, err := h.AuthService.ConfigureEmailOTP(req.Context(), req.AuthUser.User.ID, enable); err != nil {
return err
} else {
req.NewAlerts = append(req.NewAlerts, request.Alert{
Type: "primary",
Text: "Two factor authentication with TOTP disabled",
})
// Make sure we update User's data in the session
req.AuthUser.User = user
req.AuthUser.Save(req.Session)
h.Log.Info("email OTP configured", zap.Bool("enabled", enable))
}
}
return nil
}

View File

@@ -101,7 +101,7 @@ func (h *AuthHandlers) sessionsProc(req *request.AuthReq) error {
func (h *AuthHandlers) getSessions(req *request.AuthReq) (ss userSessions, err error) {
var set []*types.AuthSession
if set, err = h.SessionManager.Search(req.Context(), req.User.ID); err != nil {
if set, err = h.SessionManager.Search(req.Context(), req.AuthUser.User.ID); err != nil {
return
} else {
ss = make(userSessions, len(set))

View File

@@ -38,7 +38,14 @@ func (h *AuthHandlers) signupProc(req *request.AuthReq) error {
h.Log.Info("signup successful")
req.RedirectTo = GetLinks().Profile
h.storeUserToSession(req, newUser)
req.AuthUser = request.NewAuthUser(h.Settings, newUser, false, 0)
// auto-complete EmailOTP
req.AuthUser.CompleteEmailOTP()
req.AuthUser.Save(req.Session)
} else {
req.RedirectTo = GetLinks().PendingEmailConfirmation
}
@@ -73,8 +80,8 @@ func (h *AuthHandlers) signupProc(req *request.AuthReq) error {
func (h *AuthHandlers) pendingEmailConfirmation(req *request.AuthReq) error {
req.Template = TmplPendingEmailConfirmation
if _, has := req.Request.URL.Query()["resend"]; has && req.User != nil {
err := h.AuthService.SendEmailAddressConfirmationToken(req.Context(), req.User)
if _, has := req.Request.URL.Query()["resend"]; has && req.AuthUser.User != nil {
err := h.AuthService.SendEmailAddressConfirmationToken(req.Context(), req.AuthUser.User)
if err != nil {
return err
}
@@ -96,7 +103,14 @@ func (h *AuthHandlers) confirmEmail(req *request.AuthReq) (err error) {
})
req.RedirectTo = GetLinks().Profile
h.storeUserToSession(req, user)
req.AuthUser = request.NewAuthUser(h.Settings, user, false, 0)
// auto-complete EmailOTP
req.AuthUser.CompleteEmailOTP()
req.AuthUser.Save(req.Session)
return nil
}
}
@@ -106,7 +120,7 @@ func (h *AuthHandlers) confirmEmail(req *request.AuthReq) (err error) {
// redirect to the right page
// not doing this here and relying on handler on subseq. request
// will cause alerts to be removed
if req.User == nil {
if req.AuthUser.User == nil {
req.RedirectTo = GetLinks().Login
} else {
req.RedirectTo = GetLinks().Profile

View File

@@ -5,7 +5,6 @@ import (
"encoding/gob"
"github.com/cortezaproject/corteza-server/auth/external"
"github.com/cortezaproject/corteza-server/auth/request"
"github.com/cortezaproject/corteza-server/auth/session"
"github.com/cortezaproject/corteza-server/auth/settings"
"github.com/cortezaproject/corteza-server/pkg/auth"
"github.com/cortezaproject/corteza-server/pkg/options"
@@ -34,6 +33,14 @@ type (
SendEmailAddressConfirmationToken(ctx context.Context, u *types.User) (err error)
SendPasswordResetToken(ctx context.Context, email string) (err error)
GetProviders() types.ExternalAuthProviderSet
ValidateTOTP(ctx context.Context, code string) (err error)
ConfigureTOTP(ctx context.Context, secret string, code string) (u *types.User, err error)
RemoveTOTP(ctx context.Context, userID uint64, code string) (u *types.User, err error)
SendEmailOTP(ctx context.Context) (err error)
ConfigureEmailOTP(ctx context.Context, userID uint64, enable bool) (u *types.User, err error)
ValidateEmailOTP(ctx context.Context, code string) (err error)
}
userService interface {
@@ -62,7 +69,7 @@ type (
Templates templateExecutor
OAuth2 *oauth2server.Server
SessionManager *session.Manager
SessionManager *request.SessionManager
AuthService authService
UserService userService
ClientService clientService
@@ -89,6 +96,9 @@ const (
TmplSessions = "sessions.html.tpl"
TmplSignup = "signup.html.tpl"
TmplPendingEmailConfirmation = "pending-email-confirmation.html.tpl"
TmplMfa = "mfa.html.tpl"
TmplMfaTotp = "mfa-totp.html.tpl"
TmplMfaTotpDisable = "mfa-totp-disable.html.tpl"
TmplInternalError = "error-internal.html.tpl"
)
@@ -99,15 +109,6 @@ func init() {
gob.Register(url.Values{})
}
// Stores user & roles
//
// We need to store roles separately because they do not get serialized alongside with user
// due to unexported field
func (h *AuthHandlers) storeUserToSession(req *request.AuthReq, u *types.User) {
session.SetUser(req.Session, u)
session.SetRoleMemberships(req.Session, u.Roles())
}
// handles auth request and prepares request struct with request, session and response helper
func (h *AuthHandlers) handle(fn handlerFn) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
@@ -133,15 +134,15 @@ func (h *AuthHandlers) handle(fn handlerFn) http.HandlerFunc {
return
}
req.Client = session.GetOauth2Client(req.Session)
req.Client = request.GetOauth2Client(req.Session)
req.User = session.GetUser(req.Session)
req.AuthUser = request.GetAuthUser(req.Session)
// make sure user (identity) is part of the context
// so we can properly identify ourselves when interacting
// with services
if req.User != nil {
req.Request = req.Request.Clone(auth.SetIdentityToContext(req.Context(), req.User))
if req.AuthUser != nil && !req.AuthUser.PendingMFA() {
req.Request = req.Request.Clone(auth.SetIdentityToContext(req.Context(), req.AuthUser.User))
}
// Alerts show for 1 session only!
@@ -151,6 +152,12 @@ func (h *AuthHandlers) handle(fn handlerFn) http.HandlerFunc {
return
}
if req.RedirectTo != "" && len(req.PrevAlerts) > 0 {
// redirect happened, so probably none noticed alerts
// lets push them at the end of new alerts
req.NewAlerts = append(req.NewAlerts, req.PrevAlerts...)
}
if len(req.NewAlerts) > 0 {
req.SetAlerts(req.NewAlerts...)
}
@@ -218,7 +225,9 @@ func (h *AuthHandlers) handle(fn handlerFn) http.HandlerFunc {
// Add alerts, settings, providers, csrf token
func (h *AuthHandlers) enrichTmplData(req *request.AuthReq) interface{} {
d := req.Data
req.Data["user"] = req.User
if req.AuthUser != nil {
req.Data["user"] = req.AuthUser.User
}
if req.Client != nil {
c := authClient{
@@ -270,10 +279,52 @@ func (h *AuthHandlers) enrichTmplData(req *request.AuthReq) interface{} {
return d
}
// Handle successful auth (on any factor)
func handleSuccessfulAuth(req *request.AuthReq) {
switch {
case req.AuthUser.PendingMFA():
req.RedirectTo = GetLinks().Mfa
case request.GetOAuth2AuthParams(req.Session) != nil:
// client authorization flow was paused, continue.
req.RedirectTo = GetLinks().OAuth2AuthorizeClient
default:
// Always go to profile
req.RedirectTo = GetLinks().Profile
}
}
// redirects anonymous users to login
func authOnly(fn handlerFn) handlerFn {
return func(req *request.AuthReq) error {
if req.User == nil {
// these next few lines keep users away from the pages they should not see
// and redirect them to where they need to be
switch {
case req.AuthUser == nil || req.AuthUser.User == nil:
// not authenticated at all, move to login
req.RedirectTo = GetLinks().Login
case req.AuthUser.UnconfiguredTOTP():
// authenticated but need to configure MFA
req.RedirectTo = GetLinks().MfaTotpNewSecret
case req.AuthUser.PendingMFA():
// authenticated but MFA pending
req.RedirectTo = GetLinks().Mfa
default:
return fn(req)
}
return nil
}
}
func partAuthOnly(fn handlerFn) handlerFn {
return func(req *request.AuthReq) error {
if req.AuthUser == nil || req.AuthUser.User == nil {
req.RedirectTo = GetLinks().Login
return nil
} else {
@@ -285,7 +336,7 @@ func authOnly(fn handlerFn) handlerFn {
// redirects authenticated users to profile
func anonyOnly(fn handlerFn) handlerFn {
return func(req *request.AuthReq) error {
if req.User != nil {
if req.AuthUser != nil && req.AuthUser.User != nil {
req.RedirectTo = GetLinks().Profile
return nil
} else {

View File

@@ -24,6 +24,12 @@ type (
OAuth2Info,
OAuth2DefaultClient,
Mfa,
MfaTotpNewSecret,
MfaTotpQRImage,
MfaTotpDisable,
External,
Assets string
@@ -52,6 +58,11 @@ func GetLinks() Links {
OAuth2Info: "/auth/oauth2/info",
OAuth2DefaultClient: "/auth/oauth2/default-client",
Mfa: "/auth/mfa",
MfaTotpNewSecret: "/auth/mfa/totp/setup",
MfaTotpQRImage: "/auth/mfa/totp/qr.png",
MfaTotpDisable: "/auth/mfa/totp/disable",
External: "/auth/external",
Assets: "/auth/assets/public",

View File

@@ -63,15 +63,26 @@ func (h *AuthHandlers) MountHttpRoutes(r chi.Router) {
r.Get(l.Login, h.handle(anonyOnly(h.loginForm)))
r.Post(l.Login, h.handle(h.onlyIfLocalEnabled(anonyOnly(h.loginProc))))
r.Get(l.Mfa, h.handle(h.mfaForm))
r.Post(l.Mfa, h.handle(h.mfaProc))
r.Get(l.RequestPasswordReset, h.handle(h.onlyIfPasswordResetEnabled(anonyOnly(h.requestPasswordResetForm))))
r.Post(l.RequestPasswordReset, h.handle(h.onlyIfPasswordResetEnabled(anonyOnly(h.requestPasswordResetProc))))
r.Get(l.PasswordResetRequested, h.handle(h.onlyIfPasswordResetEnabled(anonyOnly(h.passwordResetRequested))))
r.Get(l.ResetPassword, h.handle(h.onlyIfPasswordResetEnabled(h.resetPasswordForm)))
r.Post(l.ResetPassword, h.handle(h.onlyIfPasswordResetEnabled(authOnly(h.resetPasswordProc))))
r.Get(l.Security, h.handle(authOnly(h.security)))
r.Get(l.Security, h.handle(authOnly(h.securityForm)))
r.Post(l.Security, h.handle(authOnly(h.securityProc)))
r.Get(l.ChangePassword, h.handle(h.onlyIfLocalEnabled(authOnly(h.changePasswordForm))))
r.Post(l.ChangePassword, h.handle(h.onlyIfLocalEnabled(authOnly(h.changePasswordProc))))
r.Get(l.MfaTotpNewSecret, h.handle(partAuthOnly(h.mfaTotpConfigForm)))
r.Post(l.MfaTotpNewSecret, h.handle(partAuthOnly(h.mfaTotpConfigProc)))
r.Get(l.MfaTotpQRImage, h.handle(partAuthOnly(h.mfaTotpConfigQR)))
r.Get(l.MfaTotpDisable, h.handle(authOnly(h.mfaTotpDisableForm)))
r.Post(l.MfaTotpDisable, h.handle(authOnly(h.mfaTotpDisableProc)))
})
r.Group(func(r chi.Router) {
@@ -92,5 +103,4 @@ func (h *AuthHandlers) MountHttpRoutes(r chi.Router) {
r.HandleFunc("/auth/oauth2/token", h.handle(h.oauth2Token))
r.HandleFunc("/auth/oauth2/info", h.oauth2Info)
})
}

View File

@@ -2,41 +2,41 @@ package oauth2
import (
"fmt"
"github.com/cortezaproject/corteza-server/auth/session"
"github.com/cortezaproject/corteza-server/auth/request"
"github.com/go-oauth2/oauth2/v4/server"
"net/http"
)
func NewUserAuthorizer(sm *session.Manager, loginURL, clientAuthURL string) server.UserAuthorizationHandler {
func NewUserAuthorizer(sm *request.SessionManager, loginURL, clientAuthURL string) server.UserAuthorizationHandler {
return func(w http.ResponseWriter, r *http.Request) (identity string, err error) {
var (
ses = sm.Get(r)
user = session.GetUser(ses)
client = session.GetOauth2Client(ses)
au = request.GetAuthUser(ses)
client = request.GetOauth2Client(ses)
)
// temporary break oauth2 flow by redirecting to
// login form and ask user to authenticate
session.SetOauth2AuthParams(ses, r.Form)
request.SetOauth2AuthParams(ses, r.Form)
// make sure session is saved!
sm.Save(w, r)
// @todo harden security by enforcing login
// for each new authorization flow
if user == nil {
if au == nil {
// user is currently not logged-in;
http.Redirect(w, r, loginURL, http.StatusSeeOther)
return
} else {
if !session.IsOauth2ClientAuthorized(ses) || client == nil {
if !request.IsOauth2ClientAuthorized(ses) || client == nil {
// user logged in but we need to re-authenticate the client
http.Redirect(w, r, clientAuthURL, http.StatusSeeOther)
return
}
}
var roles = session.GetRoleMemberships(ses)
var roles = request.GetRoleMemberships(ses)
if client.Security != nil {
// filter user's roles with client security settings
roles = client.Security.ProcessRoles(roles...)
@@ -44,14 +44,14 @@ func NewUserAuthorizer(sm *session.Manager, loginURL, clientAuthURL string) serv
// User authenticated, client authorized!
// remove authorization values from session
session.SetOauth2AuthParams(ses, nil)
session.SetOauth2Client(ses, nil)
session.SetOauth2ClientAuthorized(ses, false)
request.SetOauth2AuthParams(ses, nil)
request.SetOauth2Client(ses, nil)
request.SetOauth2ClientAuthorized(ses, false)
// make sure session is saved!
sm.Save(w, r)
return UserIDSerializer(user.ID, roles...), nil
return UserIDSerializer(au.User.ID, roles...), nil
}
}

163
auth/request/auth_user.go Normal file
View File

@@ -0,0 +1,163 @@
package request
import (
"encoding/gob"
"github.com/cortezaproject/corteza-server/auth/settings"
"github.com/cortezaproject/corteza-server/system/types"
"github.com/gorilla/sessions"
"time"
)
type (
// handles authentication state, keeps info about permanent login and
// status of the MFA
authUser struct {
User *types.User
PermSession bool
PermLifetime time.Duration
MFAStatus map[authType]authStatus
}
authStatus uint
authType string
)
const (
// not enforced by user or by global settings
authStatusDisabled authStatus = iota
// when mfa is unconfigured on user but enforced globally
// this can happen on when new user registers or when global enforcement is later enabled
authStatusUnconfigured
// enforced by user or global settings, but not yet verified
authStatusPending
// verified
authStatusOK
)
const (
// password does not really have any function,
// more for demonstration purposes
authByPassword = "password"
authByEmailOTP = "email-otp"
authByTOTP = "totp"
)
func init() {
gob.Register(&authUser{})
}
func NewAuthUser(s *settings.Settings, u *types.User, perm bool, permLifetime time.Duration) *authUser {
au := &authUser{
User: u,
PermSession: perm,
PermLifetime: permLifetime,
}
au.set(s, u)
return au
}
func (au *authUser) Update(s *settings.Settings, u *types.User) {
au.set(s, u)
}
func (au *authUser) set(s *settings.Settings, u *types.User) {
// User's MFA security policy
umsp := u.Meta.SecurityPolicy.MFA
// Global MFA security policy
gmsp := s.MultiFactor
mfaStatus := map[authType]authStatus{
authByPassword: authStatusOK,
authByEmailOTP: authStatusDisabled,
authByTOTP: authStatusDisabled,
}
// determinate mfa status for email OTP
if !gmsp.EmailOTP.Enabled {
mfaStatus[authByEmailOTP] = authStatusDisabled
} else if umsp.EnforcedEmailOTP || gmsp.EmailOTP.Enforced {
mfaStatus[authByEmailOTP] = authStatusPending
}
// determinate mfa status for TOTP
if !gmsp.TOTP.Enabled {
mfaStatus[authByTOTP] = authStatusDisabled
} else if !umsp.EnforcedTOTP && gmsp.TOTP.Enforced {
// TOTP not enforced on user but enforced globally
mfaStatus[authByTOTP] = authStatusUnconfigured
} else if gmsp.TOTP.Enforced {
mfaStatus[authByTOTP] = authStatusPending
}
au.MFAStatus = mfaStatus
}
func (au authUser) DisabledEmailOTP() bool {
return au.MFAStatus[authByEmailOTP] == authStatusDisabled
}
func (au authUser) PendingEmailOTP() bool {
return au.MFAStatus[authByEmailOTP] == authStatusPending
}
func (au authUser) DisabledTOTP() bool {
return au.MFAStatus[authByTOTP] == authStatusDisabled
}
func (au authUser) UnconfiguredTOTP() bool {
return au.MFAStatus[authByTOTP] == authStatusUnconfigured
}
func (au authUser) PendingTOTP() bool {
return au.MFAStatus[authByTOTP] == authStatusPending
}
// PendingMFA Returns true if any of MFAs are pending
func (au authUser) PendingMFA() bool {
for _, st := range au.MFAStatus {
if st == authStatusPending {
return true
}
}
return false
}
func (au *authUser) CompleteEmailOTP() {
au.MFAStatus[authByEmailOTP] = authStatusOK
}
func (au *authUser) CompleteTOTP() {
au.MFAStatus[authByTOTP] = authStatusOK
}
func (au *authUser) ResetTOTP() {
au.MFAStatus[authByTOTP] = authStatusUnconfigured
}
func (au *authUser) Forget(ses *sessions.Session) {
delete(ses.Values, keyAuthUser)
delete(ses.Values, keyPermanent)
}
func (au *authUser) Save(ses *sessions.Session) {
ses.Values[keyAuthUser] = au
// explicitly save roles
ses.Values[keyRoles] = au.User.Roles()
if au.PermLifetime > 0 {
ses.Options.MaxAge = int(au.PermLifetime / time.Second)
ses.Values[keyPermanent] = true
} else {
ses.Options.MaxAge = 0
delete(ses.Values, keyPermanent)
}
}

View File

@@ -19,8 +19,8 @@ type (
// Sessions
Session *sessions.Session
// Loaded user (from session)
User *types.User
// Authenticated user
AuthUser *authUser
// Current client (when in oauth2 flow)
Client *types.AuthClient
@@ -76,6 +76,14 @@ func (req *AuthReq) SetInternalError(err error) bool {
return true
}
func (req *AuthReq) PushAlert(text string) {
req.NewAlerts = append(req.NewAlerts, Alert{Type: "primary", Text: text})
}
func (req *AuthReq) PushDangerAlert(text string) {
req.NewAlerts = append(req.NewAlerts, Alert{Type: "danger", Text: text})
}
func (req *AuthReq) PopAlerts() []Alert {
val, has := req.Session.Values["alerts"]
if !has {

View File

@@ -1,11 +1,10 @@
package session
package request
import (
"bytes"
"context"
"encoding/gob"
"fmt"
"github.com/cortezaproject/corteza-server/auth/request"
"github.com/cortezaproject/corteza-server/pkg/options"
"github.com/cortezaproject/corteza-server/pkg/rand"
"github.com/cortezaproject/corteza-server/store"
@@ -183,11 +182,11 @@ func (s cortezaSessionStore) save(ctx context.Context, ses *sessions.Session) (e
// new session does not belong to anyone yet.
// retrieve user id from ses. values
if user := GetUser(ses); user != nil {
cortezaSession.UserID = user.ID
if au := GetAuthUser(ses); au != nil {
cortezaSession.UserID = au.User.ID
}
extra := request.GetExtraReqInfoFromContext(ctx)
extra := GetExtraReqInfoFromContext(ctx)
cortezaSession.UserAgent = extra.UserAgent
cortezaSession.RemoteAddr = extra.RemoteAddr

View File

@@ -1,4 +1,4 @@
package session
package request
import (
"context"
@@ -11,7 +11,7 @@ import (
)
type (
Manager struct {
SessionManager struct {
cstore store.AuthSessions
sstore sessions.Store
opt options.AuthOpt
@@ -19,28 +19,28 @@ type (
}
)
func NewManager(store store.AuthSessions, opt options.AuthOpt, log *zap.Logger) *Manager {
m := &Manager{opt: opt, log: log}
func NewSessionManager(store store.AuthSessions, opt options.AuthOpt, log *zap.Logger) *SessionManager {
m := &SessionManager{opt: opt, log: log}
m.cstore = store
m.sstore = CortezaSessionStore(store, opt)
return m
}
func (m Manager) Store() sessions.Store { return m.sstore }
func (m SessionManager) Store() sessions.Store { return m.sstore }
func (m *Manager) Get(r *http.Request) *sessions.Session {
func (m *SessionManager) Get(r *http.Request) *sessions.Session {
ses, _ := m.sstore.Get(r, m.opt.SessionCookieName)
return ses
}
func (m *Manager) Save(w http.ResponseWriter, r *http.Request) {
func (m *SessionManager) Save(w http.ResponseWriter, r *http.Request) {
if err := m.Get(r).Save(r, w); err != nil {
m.log.Warn("failed to save sessions", zap.Error(err))
}
}
// Returns all users sessions
func (m *Manager) Search(ctx context.Context, userID uint64) (set []*types.AuthSession, err error) {
func (m *SessionManager) Search(ctx context.Context, userID uint64) (set []*types.AuthSession, err error) {
set, _, err = m.cstore.SearchAuthSessions(ctx, types.AuthSessionFilter{UserID: userID})
for i := range set {
set[i].Data = nil
@@ -49,11 +49,11 @@ func (m *Manager) Search(ctx context.Context, userID uint64) (set []*types.AuthS
}
// Returns all users sessions
func (m *Manager) DeleteByUserID(ctx context.Context, userID uint64) (err error) {
func (m *SessionManager) DeleteByUserID(ctx context.Context, userID uint64) (err error) {
return m.cstore.DeleteAuthSessionsByUserID(ctx, userID)
}
// Returns all users sessions
func (m *Manager) DeleteByID(ctx context.Context, sessionID string) (err error) {
func (m *SessionManager) DeleteByID(ctx context.Context, sessionID string) (err error) {
return m.cstore.DeleteAuthSessionByID(ctx, sessionID)
}

View File

@@ -1,17 +1,15 @@
package session
package request
import (
"github.com/cortezaproject/corteza-server/system/types"
"github.com/gorilla/sessions"
"net/url"
"time"
)
const (
keyPermanent = "permanent"
keyOriginalSession = "originalSession"
keySessionKind = "sessionKind"
keyUser = "user"
keyAuthUser = "authUser"
keyRoles = "roles"
keyOAuth2AuthParams = "oauth2AuthParams"
keyOAuth2Client = "oauth2ClientID"
@@ -19,22 +17,13 @@ const (
)
// GetUser is wrapper to get value from session
func GetUser(ses *sessions.Session) *types.User {
val, has := ses.Values[keyUser]
func GetAuthUser(ses *sessions.Session) *authUser {
val, has := ses.Values[keyAuthUser]
if !has {
return nil
}
return val.(*types.User)
}
// SetUser is a session value setting wrapper for User
func SetUser(ses *sessions.Session, val *types.User) {
if val != nil {
ses.Values[keyUser] = val
} else {
delete(ses.Values, keyUser)
}
return val.(*authUser)
}
// GetRoleMemberships is wrapper to get value from session
@@ -112,20 +101,3 @@ func SetOauth2ClientAuthorized(ses *sessions.Session, val bool) {
delete(ses.Values, keyOAuth2ClientAuthorized)
}
}
// IsOauth2ClientAuthorized is wrapper to get value from session
func IsPerm(ses *sessions.Session) bool {
_, has := ses.Values[keyPermanent]
return has
}
// SetOauth2ClientAuthorized is a session value setting wrapper for Oauth2ClientAuthorized
func SetPerm(ses *sessions.Session, ttl time.Duration) {
if ttl > 0 {
ses.Options.MaxAge = int(ttl / time.Second)
ses.Values[keyPermanent] = true
} else {
ses.Options.MaxAge = 0
delete(ses.Values, keyPermanent)
}
}

View File

@@ -8,6 +8,27 @@ type (
PasswordResetEnabled bool
ExternalEnabled bool
Providers []Provider
MultiFactor struct {
EmailOTP struct {
// Can users use email for MFA
Enabled bool
// Is MFA with email enforced?
Enforced bool
}
TOTP struct {
// Can users use TOTP MFA?
Enabled bool
// Is TOTP MFA enforced?
Enforced bool
// TOTP issuer
Issuer string
}
}
}
Provider struct {

4
go.mod
View File

@@ -3,7 +3,6 @@ module github.com/cortezaproject/corteza-server
go 1.16
require (
github.com/360EntSecGroup-Skylar/excelize/v2 v2.0.2 // indirect
github.com/766b/chi-prometheus v0.0.0-20180509160047-46ac2b31aa30
github.com/99designs/basicauth-go v0.0.0-20160802081356-2a93ba0f464d
github.com/Masterminds/goutils v1.1.0 // indirect
@@ -17,6 +16,7 @@ require (
github.com/davecgh/go-spew v1.1.1
github.com/deckarep/golang-set v1.7.1 // indirect
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/dgryski/dgoogauth v0.0.0-20190221195224-5a805980a5f3
github.com/disintegration/imaging v1.6.0
github.com/edwvee/exiffix v0.0.0-20180602190213-b57537c92a6b
github.com/fastly/go-utils v0.0.0-20180712184237-d95a45783239 // indirect
@@ -68,6 +68,7 @@ require (
go.uber.org/atomic v1.6.0
go.uber.org/zap v1.15.0
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a // indirect
golang.org/x/sys v0.0.0-20200918174421-af09f7315aff // indirect
google.golang.org/grpc v1.32.0
google.golang.org/protobuf v1.25.0
@@ -78,4 +79,5 @@ require (
gopkg.in/square/go-jose.v2 v2.3.1 // indirect
gopkg.in/yaml.v2 v2.3.0
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776
rsc.io/qr v0.2.0
)

33
go.sum
View File

@@ -1,11 +1,8 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.30.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.44.3 h1:0sMegbmn/8uTwpNkB0q9cLEpZ2W5a6kl+wtBQgPWBJQ=
cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
@@ -36,8 +33,6 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/360EntSecGroup-Skylar/excelize/v2 v2.0.2 h1:StMrA6UQ5Cm6206DxXGuV/NMqSIOIDoMXMYt8JPe1lE=
github.com/360EntSecGroup-Skylar/excelize/v2 v2.0.2/go.mod h1:EfRHD2k+Kd7ijnqlwOrH1IifwgWB9yYJ0pdXtBZmlpU=
github.com/766b/chi-prometheus v0.0.0-20180509160047-46ac2b31aa30 h1:bNHbCMKiQxpRNe4Pk2W09N1aXXc4ICOawQFKIDEicqc=
github.com/766b/chi-prometheus v0.0.0-20180509160047-46ac2b31aa30/go.mod h1:X/LhbmoBoRu8TxoGIOIraVNhfz3hhikJoaelrOuhdPY=
github.com/99designs/basicauth-go v0.0.0-20160802081356-2a93ba0f464d h1:j6oB/WPCigdOkxtuPl1VSIiLpy7Mdsu6phQffbF19Ng=
@@ -93,6 +88,8 @@ github.com/deckarep/golang-set v1.7.1 h1:SCQV0S6gTtp6itiFrTqI+pfmJ4LN85S1YzhDf9r
github.com/deckarep/golang-set v1.7.1/go.mod h1:93vsz/8Wt4joVM7c2AVqh+YRMiUSc14yDtF28KmMOgQ=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/dgoogauth v0.0.0-20190221195224-5a805980a5f3 h1:AqeKSZIG/NIC75MNQlPy/LM3LxfpLwahICJBHwSMFNc=
github.com/dgryski/dgoogauth v0.0.0-20190221195224-5a805980a5f3/go.mod h1:hEfFauPHz7+NnjR/yHJGhrKo1Za+zStgwUETx3yzqgY=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/disintegration/imaging v1.6.0 h1:nVPXRUUQ36Z7MNf0O77UzgnOb1mkMMor7lmJMJXc/mA=
github.com/disintegration/imaging v1.6.0/go.mod h1:xuIt+sRxDFrHS0drzXUlCJthkJ8k7lkkUojDSR247MQ=
@@ -147,7 +144,6 @@ github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4er
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1 h1:qGJ6qTW+x6xX/my+8YUVl4WNpX9B7+/l2tRsHGZ7f2s=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
@@ -176,8 +172,8 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1 h1:JFrFEBb2xKufg6XkJsJr+WbKb4FQlURi5RUcBveYu9k=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
@@ -262,8 +258,6 @@ github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.1.0 h1:/5u4a+KGJptBRqGzPvYQL9p0d/tPR4S31+Tnzj9lEO4=
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/markbates/going v1.0.0/go.mod h1:I6mnB4BPnEeqo85ynXIx1ZFLLbtiLHNXVgWeFO9OGOA=
github.com/markbates/goth v1.50.0 h1:KCAErbDdHh11gQAJs/GV73LCv4NwA7Z6wNZAU32ggMc=
github.com/markbates/goth v1.50.0/go.mod h1:zZmAw0Es0Dpm7TT/4AdN14QrkiWLMrrU9Xei1o+/mdA=
github.com/markbates/goth v1.67.1 h1:gU5B0pzHVyhnJPwGynfFnkfvaQ39C1Sy+ewdl+bhAOw=
github.com/markbates/goth v1.67.1/go.mod h1:EyLFHGU5ySr2GXRDyJH5nu2dA7parbC8QwIYW/rGcWg=
github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
@@ -283,8 +277,6 @@ github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY=
github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
github.com/moul/http2curl v1.0.0 h1:dRMWoAtb+ePxMlLkrCbAqh4TlPHXvoGUSQ323/9Zahs=
github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ=
github.com/mrjones/oauth v0.0.0-20180629183705-f4e24b6d100c/go.mod h1:skjdDftzkFALcuGzYSklqYd8gvat6F1gZJ4YPVbkZpM=
@@ -444,10 +436,10 @@ golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTk
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b h1:Wh+f8QHJXR411sJR8/vRBTZ7YapZaRvUcLFFJhusH0k=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
@@ -456,6 +448,7 @@ golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -485,19 +478,16 @@ golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344 h1:vGXIOMxbNfDTk/aXCmfdLgkrSV+Z2tcbze+pEc3v5W4=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200927032502-5d4f70055728/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200930145003-4acb6c075d10 h1:YfxMZzv3PjGonQYNUaeU2+DhAdqOxerQ30JFB6WgAXo=
golang.org/x/net v0.0.0-20200930145003-4acb6c075d10/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/oauth2 v0.0.0-20180620175406-ef147856a6dd/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43 h1:ld7aEMNHoBnnDAX15v1T6z31v8HwR2A9FYOuAhWqkwc=
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
@@ -551,7 +541,6 @@ golang.org/x/sys v0.0.0-20200918174421-af09f7315aff/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@@ -575,7 +564,6 @@ golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtn
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5 h1:hKsoRgsbwY1NafxrwTs+k64bikrLBkAgPir1TNCj3Zs=
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
@@ -603,11 +591,12 @@ golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
golang.org/x/tools v0.0.0-20200929161345-d7fc70abf50f h1:18s2P7JILnVhIF2+ZtGJQ9czV5bvTsb13/UGtNPDbjA=
golang.org/x/tools v0.0.0-20200929161345-d7fc70abf50f/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
@@ -627,7 +616,6 @@ google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSr
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
google.golang.org/api v0.32.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
@@ -658,7 +646,6 @@ google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfG
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 h1:+kGHl1aib/qcwaRi1CbqBZ1rk19r85MNUf8HaBghugY=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
@@ -717,18 +704,18 @@ gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200601152816-913338de1bd2 h1:VEmvx0P+GVTgkNu2EdTN988YCZPcD3lo9AoczZpucwc=
gopkg.in/yaml.v3 v3.0.0-20200601152816-913338de1bd2/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4 h1:UoveltGrhghAA7ePc+e+QYDHXrBps2PqFZiHkGR/xK8=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY=
rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=

View File

@@ -2,12 +2,15 @@ package rdbms
import (
"github.com/Masterminds/squirrel"
"github.com/cortezaproject/corteza-server/pkg/filter"
"github.com/cortezaproject/corteza-server/system/types"
)
func (s Store) convertCredentialsFilter(f types.CredentialsFilter) (query squirrel.SelectBuilder, err error) {
query = s.credentialsSelectBuilder()
query = filter.StateCondition(query, "crd.deleted_at", f.Deleted)
if f.Kind != "" {
query = query.Where(squirrel.Eq{"crd.kind": f.Kind})
}

View File

@@ -7,14 +7,17 @@ import (
internalAuth "github.com/cortezaproject/corteza-server/pkg/auth"
"github.com/cortezaproject/corteza-server/pkg/errors"
"github.com/cortezaproject/corteza-server/pkg/eventbus"
"github.com/cortezaproject/corteza-server/pkg/filter"
"github.com/cortezaproject/corteza-server/pkg/handle"
"github.com/cortezaproject/corteza-server/pkg/rand"
"github.com/cortezaproject/corteza-server/pkg/rbac"
"github.com/cortezaproject/corteza-server/store"
"github.com/cortezaproject/corteza-server/system/service/event"
"github.com/cortezaproject/corteza-server/system/types"
"github.com/dgryski/dgoogauth"
"github.com/markbates/goth"
"golang.org/x/crypto/bcrypt"
rand2 "math/rand"
"regexp"
"strconv"
"time"
@@ -35,6 +38,7 @@ type (
authAccessController interface {
CanImpersonateUser(context.Context, *types.User) bool
CanUpdateUser(context.Context, *types.User) bool
}
)
@@ -44,7 +48,8 @@ const (
credentialsTypeEmailAuthToken = "email-authentication-token"
credentialsTypeResetPasswordToken = "password-reset-token"
credentialsTypeResetPasswordTokenExchanged = "password-reset-token-exchanged"
credentialsTypeAuthToken = "auth-token"
credentialsTypeMfaTotpSecret = "mfa-totp-secret"
credentialsTypeMFAEmailOTP = "mfa-email-otp"
credentialsTokenLength = 32
)
@@ -639,31 +644,6 @@ func (svc auth) SetPasswordCredentials(ctx context.Context, userID uint64, passw
return store.CreateCredentials(ctx, svc.store, c)
}
// IssueAuthRequestToken returns token that can be used for authentication
func (svc auth) IssueAuthRequestToken(ctx context.Context, user *types.User) (token string, err error) {
return svc.createUserToken(ctx, user, credentialsTypeAuthToken)
}
// ValidateAuthRequestToken returns user that requested auth token
func (svc auth) ValidateAuthRequestToken(ctx context.Context, token string) (u *types.User, err error) {
var (
aam = &authActionProps{
credentials: &types.Credentials{Kind: credentialsTypeAuthToken},
}
)
err = func() error {
u, err = svc.loadUserFromToken(ctx, token, credentialsTypeAuthToken)
if err != nil && u != nil {
aam.setUser(u)
ctx = internalAuth.SetIdentityToContext(ctx, u)
}
return err
}()
return u, svc.recordAction(ctx, aam, AuthActionValidateToken, err)
}
// ValidateEmailConfirmationToken issues a validation token that can be used for
func (svc auth) ValidateEmailConfirmationToken(ctx context.Context, token string) (user *types.User, err error) {
return svc.loadFromTokenAndConfirmEmail(ctx, token, credentialsTypeEmailAuthToken)
@@ -925,12 +905,20 @@ func (svc auth) createUserToken(ctx context.Context, u *types.User, kind string)
err = func() error {
switch kind {
case credentialsTypeAuthToken:
// 15 sec expiration for all tokens that are part of redirection
expiresAt = now().Add(time.Second * 15)
case credentialsTypeMFAEmailOTP:
expSec := svc.settings.Auth.MultiFactor.EmailOTP.Expires
if expSec == 0 {
expSec = 60
}
expiresAt = now().Add(time.Second * time.Duration(expSec))
// random number, 6 chars
token = fmt.Sprintf("%06d", rand2.Int())[0:6]
default:
// 1h expiration for all tokens send via email
expiresAt = now().Add(time.Minute * 60)
token = string(rand.Bytes(credentialsTokenLength))
}
c := &types.Credentials{
@@ -938,17 +926,25 @@ func (svc auth) createUserToken(ctx context.Context, u *types.User, kind string)
CreatedAt: *now(),
OwnerID: u.ID,
Kind: kind,
Credentials: string(rand.Bytes(credentialsTokenLength)),
Credentials: token,
ExpiresAt: &expiresAt,
}
err := store.CreateCredentials(ctx, svc.store, c)
err = store.CreateCredentials(ctx, svc.store, c)
if err != nil {
return err
}
token = fmt.Sprintf("%s%d", c.Credentials, c.ID)
switch kind {
case credentialsTypeMFAEmailOTP:
// do not alter the final token
default:
// suffixing tokens with credentials ID
// this will help us with token lookups
token = fmt.Sprintf("%s%d", token, c.ID)
}
return nil
}()
@@ -978,6 +974,339 @@ func (svc auth) autoPromote(ctx context.Context, u *types.User) (err error) {
return svc.recordAction(ctx, aam, AuthActionAutoPromote, err)
}
// ValidateTOTP checks given code with the current secret
// Fn fails if no secret is set
func (svc auth) ValidateTOTP(ctx context.Context, code string) (err error) {
var (
c *types.Credentials
u *types.User
kind = credentialsTypeMfaTotpSecret
aam = &authActionProps{credentials: &types.Credentials{Kind: kind}}
i = internalAuth.GetIdentityFromContext(ctx)
)
err = svc.store.Tx(ctx, func(ctx context.Context, s store.Storer) error {
if !svc.settings.Auth.MultiFactor.TOTP.Enabled {
return AuthErrDisabledMFAWithTOTP()
}
u, err = store.LookupUserByID(ctx, svc.store, i.Identity())
if errors.IsNotFound(err) {
return AuthErrFailedForUnknownUser(aam)
}
aam.setUser(u)
if !u.Meta.SecurityPolicy.MFA.EnforcedTOTP {
return AuthErrUnconfiguredTOTP()
}
if c, err = svc.getTOTPSecret(ctx, s, u.ID); err != nil {
return err
} else if err = svc.validateTOTP(c.Credentials, code); err != nil {
return err
} else {
c.LastUsedAt = now()
return store.UpdateCredentials(ctx, s, c)
}
})
return svc.recordAction(ctx, aam, AuthActionTotpValidate, err)
}
// ConfigureTOTP stores totp secret in user's credentials
//
// It returns the user with security policy changes
func (svc auth) ConfigureTOTP(ctx context.Context, secret string, code string) (u *types.User, err error) {
var (
kind = credentialsTypeMfaTotpSecret
aam = &authActionProps{credentials: &types.Credentials{Kind: kind}}
i = internalAuth.GetIdentityFromContext(ctx)
)
err = svc.store.Tx(ctx, func(ctx context.Context, s store.Storer) error {
if !svc.settings.Auth.MultiFactor.TOTP.Enabled {
return AuthErrDisabledMFAWithTOTP()
}
if err = svc.validateTOTP(secret, code); err != nil {
return err
}
u, err = store.LookupUserByID(ctx, svc.store, i.Identity())
if errors.IsNotFound(err) {
return AuthErrFailedForUnknownUser(aam)
}
aam.setUser(u)
if i == nil || u.Meta.SecurityPolicy.MFA.EnforcedTOTP {
// TOTP is already enforced on the user,
// this means that we can not just allow the change
return AuthErrNotAllowedToConfigureTOTP()
}
// revoke (soft-delete) all existing secrets
if err = svc.revokeAllTOTP(ctx, s, u.ID); err != nil {
return err
}
cred := &types.Credentials{
ID: nextID(),
CreatedAt: *now(),
OwnerID: u.ID,
Kind: kind,
Credentials: secret,
}
if err = store.CreateCredentials(ctx, s, cred); err != nil {
return err
}
u.Meta.SecurityPolicy.MFA.EnforcedTOTP = true
return store.UpdateUser(ctx, s, u)
})
return u, svc.recordAction(ctx, aam, AuthActionTotpConfigure, err)
}
// RemoveTOTP removes TOTP secret from user's credentials
//
// If user is removing own TOTP code is required
// When removing TOTP for another user, remover shou
//
// It returns the user with security policy changes
func (svc auth) RemoveTOTP(ctx context.Context, userID uint64, code string) (u *types.User, err error) {
var (
c *types.Credentials
kind = credentialsTypeMfaTotpSecret
aam = &authActionProps{credentials: &types.Credentials{Kind: kind}}
i = internalAuth.GetIdentityFromContext(ctx)
self = i != nil && i.Identity() == userID
)
err = svc.store.Tx(ctx, func(ctx context.Context, s store.Storer) error {
if !svc.settings.Auth.MultiFactor.TOTP.Enabled {
return AuthErrDisabledMFAWithTOTP()
}
if svc.settings.Auth.MultiFactor.TOTP.Enforced {
return AuthErrEnforcedMFAWithTOTP()
}
u, err = store.LookupUserByID(ctx, svc.store, userID)
if errors.IsNotFound(err) {
return AuthErrFailedForUnknownUser(aam)
}
aam.setUser(u)
if i != nil && u != nil && self {
if c, err = svc.getTOTPSecret(ctx, s, u.ID); err != nil {
return err
}
if err = svc.validateTOTP(c.Credentials, code); err != nil {
return err
}
} else if !svc.ac.CanUpdateUser(ctx, u) {
return AuthErrNotAllowedToRemoveTOTP()
}
if err = svc.revokeAllTOTP(ctx, s, u.ID); err != nil {
return err
}
u.Meta.SecurityPolicy.MFA.EnforcedTOTP = false
return store.UpdateUser(ctx, s, u)
})
return u, svc.recordAction(ctx, aam, AuthActionTotpConfigure, err)
}
// Searches for all valid TOTP secret credentials
func (svc auth) getTOTPSecret(ctx context.Context, s store.Credentials, userID uint64) (*types.Credentials, error) {
cc, _, err := store.SearchCredentials(ctx, s, types.CredentialsFilter{
OwnerID: userID,
Kind: credentialsTypeMfaTotpSecret,
Deleted: filter.StateExcluded,
})
if err != nil {
return nil, err
}
if len(cc) != 1 {
return nil, AuthErrInvalidTOTP()
}
return cc[0], nil
}
// Verifies TOTP code and secret
func (auth) validateTOTP(secret string, code string) error {
// removes all non-numeric characters
code = regexp.MustCompile(`[^0-9]`).ReplaceAllString(code, "")
if len(code) != 6 {
return AuthErrInvalidTOTP()
}
otpc := &dgoogauth.OTPConfig{
Secret: secret,
WindowSize: 5,
}
if ok, err := otpc.Authenticate(code); err != nil {
return AuthErrInvalidTOTP().Wrap(err)
} else if !ok {
return AuthErrInvalidTOTP()
}
return nil
}
// Revokes all existing user's TOTPs
func (auth) revokeAllTOTP(ctx context.Context, s store.Credentials, userID uint64) error {
// revoke (soft-delete) all existing secrets
cc, _, err := store.SearchCredentials(ctx, s, types.CredentialsFilter{
OwnerID: userID,
Kind: credentialsTypeMfaTotpSecret,
Deleted: filter.StateExcluded,
})
if err != nil {
return err
}
return cc.Walk(func(c *types.Credentials) error {
c.DeletedAt = now()
return store.UpdateCredentials(ctx, s, c)
})
}
func (svc auth) SendEmailOTP(ctx context.Context) (err error) {
var (
notificationLang = "en"
otp string
u *types.User
kind = credentialsTypeMFAEmailOTP
aam = &authActionProps{credentials: &types.Credentials{Kind: kind}}
i = internalAuth.GetIdentityFromContext(ctx)
)
err = svc.store.Tx(ctx, func(ctx context.Context, s store.Storer) (err error) {
if !svc.settings.Auth.MultiFactor.EmailOTP.Enabled {
return AuthErrDisabledMFAWithEmailOTP()
}
u, err = store.LookupUserByID(ctx, svc.store, i.Identity())
if errors.IsNotFound(err) {
return AuthErrFailedForUnknownUser(aam)
}
aam.setUser(u)
if otp, err = svc.createUserToken(ctx, u, kind); err != nil {
return
}
if err = svc.notifications.EmailOTP(ctx, notificationLang, u.Email, otp); err != nil {
return
}
return
})
return svc.recordAction(ctx, aam, AuthActionSendEmailConfirmationToken, err)
}
func (svc auth) ConfigureEmailOTP(ctx context.Context, userID uint64, enable bool) (u *types.User, err error) {
var (
kind = credentialsTypeMFAEmailOTP
aam = &authActionProps{credentials: &types.Credentials{Kind: kind}}
)
err = svc.store.Tx(ctx, func(ctx context.Context, s store.Storer) (err error) {
if !svc.settings.Auth.MultiFactor.EmailOTP.Enabled {
return AuthErrDisabledMFAWithEmailOTP()
}
if svc.settings.Auth.MultiFactor.EmailOTP.Enforced && !enable {
return AuthErrEnforcedMFAWithEmailOTP()
}
u, err = store.LookupUserByID(ctx, svc.store, userID)
if errors.IsNotFound(err) {
return AuthErrFailedForUnknownUser(aam)
}
aam.setUser(u)
u.Meta.SecurityPolicy.MFA.EnforcedEmailOTP = enable
return store.UpdateUser(ctx, s, u)
})
return u, svc.recordAction(ctx, aam, AuthActionSendEmailConfirmationToken, err)
}
// ValidateEmailOTP issues a validation OTP
func (svc auth) ValidateEmailOTP(ctx context.Context, code string) (err error) {
var (
cc types.CredentialsSet
u *types.User
kind = credentialsTypeMFAEmailOTP
aam = &authActionProps{credentials: &types.Credentials{Kind: kind}}
i = internalAuth.GetIdentityFromContext(ctx)
)
err = svc.store.Tx(ctx, func(ctx context.Context, s store.Storer) error {
if !svc.settings.Auth.MultiFactor.EmailOTP.Enabled {
return AuthErrDisabledMFAWithEmailOTP()
}
u, err = store.LookupUserByID(ctx, svc.store, i.Identity())
if errors.IsNotFound(err) {
return AuthErrFailedForUnknownUser(aam)
}
aam.setUser(u)
// removes all non-numeric characters
code = regexp.MustCompile(`[^0-9]`).ReplaceAllString(code, "")
if len(code) != 6 {
return AuthErrInvalidEmailOTP()
}
cc, _, err = store.SearchCredentials(ctx, s, types.CredentialsFilter{
OwnerID: u.ID,
Kind: kind,
Deleted: filter.StateExcluded,
})
if err != nil {
return err
}
for _, c := range cc {
if c.ExpiresAt.Before(*now()) {
continue
}
if c.Credentials != code {
continue
}
// Credentials found, remove it
return store.DeleteCredentials(ctx, s, c)
}
return AuthErrInvalidEmailOTP()
})
return svc.recordAction(ctx, aam, AuthActionEmailOtpVerify, err)
}
// LoadRoleMemberships loads membership info
//
// @todo move this to role service

View File

@@ -535,6 +535,86 @@ func AuthActionImpersonate(props ...*authActionProps) *authAction {
return a
}
// AuthActionTotpConfigure returns "system:auth.totpConfigure" action
//
// This function is auto-generated.
//
func AuthActionTotpConfigure(props ...*authActionProps) *authAction {
a := &authAction{
timestamp: time.Now(),
resource: "system:auth",
action: "totpConfigure",
log: "time-based one-time-password for {user} configured",
severity: actionlog.Notice,
}
if len(props) > 0 {
a.props = props[0]
}
return a
}
// AuthActionTotpRemove returns "system:auth.totpRemove" action
//
// This function is auto-generated.
//
func AuthActionTotpRemove(props ...*authActionProps) *authAction {
a := &authAction{
timestamp: time.Now(),
resource: "system:auth",
action: "totpRemove",
log: "time-based one-time-password for {user} removed",
severity: actionlog.Notice,
}
if len(props) > 0 {
a.props = props[0]
}
return a
}
// AuthActionTotpValidate returns "system:auth.totpValidate" action
//
// This function is auto-generated.
//
func AuthActionTotpValidate(props ...*authActionProps) *authAction {
a := &authAction{
timestamp: time.Now(),
resource: "system:auth",
action: "totpValidate",
log: "time-based one-time-password for {user} validated",
severity: actionlog.Notice,
}
if len(props) > 0 {
a.props = props[0]
}
return a
}
// AuthActionEmailOtpVerify returns "system:auth.emailOtpVerify" action
//
// This function is auto-generated.
//
func AuthActionEmailOtpVerify(props ...*authActionProps) *authAction {
a := &authAction{
timestamp: time.Now(),
resource: "system:auth",
action: "emailOtpVerify",
log: "email one-time-password for {user} verified",
severity: actionlog.Notice,
}
if len(props) > 0 {
a.props = props[0]
}
return a
}
// *********************************************************************************************************************
// *********************************************************************************************************************
// Error constructors
@@ -1137,6 +1217,276 @@ func AuthErrNotAllowedToImpersonate(mm ...*authActionProps) *errors.Error {
return e
}
// AuthErrNotAllowedToRemoveTOTP returns "system:auth.notAllowedToRemoveTOTP" as *errors.Error
//
//
// This function is auto-generated.
//
func AuthErrNotAllowedToRemoveTOTP(mm ...*authActionProps) *errors.Error {
var p = &authActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("not allowed to remove TOTP", nil),
errors.Meta("type", "notAllowedToRemoveTOTP"),
errors.Meta("resource", "system:auth"),
errors.Meta(authPropsMetaKey{}, p),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// AuthErrUnconfiguredTOTP returns "system:auth.unconfiguredTOTP" as *errors.Error
//
//
// This function is auto-generated.
//
func AuthErrUnconfiguredTOTP(mm ...*authActionProps) *errors.Error {
var p = &authActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("TOTP not configured", nil),
errors.Meta("type", "unconfiguredTOTP"),
errors.Meta("resource", "system:auth"),
errors.Meta(authPropsMetaKey{}, p),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// AuthErrNotAllowedToConfigureTOTP returns "system:auth.notAllowedToConfigureTOTP" as *errors.Error
//
//
// This function is auto-generated.
//
func AuthErrNotAllowedToConfigureTOTP(mm ...*authActionProps) *errors.Error {
var p = &authActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("not allowed to configure TOTP", nil),
errors.Meta("type", "notAllowedToConfigureTOTP"),
errors.Meta("resource", "system:auth"),
errors.Meta(authPropsMetaKey{}, p),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// AuthErrEnforcedMFAWithTOTP returns "system:auth.enforcedMFAWithTOTP" as *errors.Error
//
//
// This function is auto-generated.
//
func AuthErrEnforcedMFAWithTOTP(mm ...*authActionProps) *errors.Error {
var p = &authActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("TOTP is enforced and can not be disabled", nil),
errors.Meta("type", "enforcedMFAWithTOTP"),
errors.Meta("resource", "system:auth"),
errors.Meta(authPropsMetaKey{}, p),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// AuthErrInvalidTOTP returns "system:auth.invalidTOTP" as *errors.Error
//
//
// This function is auto-generated.
//
func AuthErrInvalidTOTP(mm ...*authActionProps) *errors.Error {
var p = &authActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("invalid TOTP", nil),
errors.Meta("type", "invalidTOTP"),
errors.Meta("resource", "system:auth"),
errors.Meta(authPropsMetaKey{}, p),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// AuthErrDisabledMFAWithTOTP returns "system:auth.disabledMFAWithTOTP" as *errors.Error
//
//
// This function is auto-generated.
//
func AuthErrDisabledMFAWithTOTP(mm ...*authActionProps) *errors.Error {
var p = &authActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("multi factor authentication with TOTP is disabled", nil),
errors.Meta("type", "disabledMFAWithTOTP"),
errors.Meta("resource", "system:auth"),
errors.Meta(authPropsMetaKey{}, p),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// AuthErrDisabledMFAWithEmailOTP returns "system:auth.disabledMFAWithEmailOTP" as *errors.Error
//
//
// This function is auto-generated.
//
func AuthErrDisabledMFAWithEmailOTP(mm ...*authActionProps) *errors.Error {
var p = &authActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("multi factor authentication with email OTP is disabled", nil),
errors.Meta("type", "disabledMFAWithEmailOTP"),
errors.Meta("resource", "system:auth"),
errors.Meta(authPropsMetaKey{}, p),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// AuthErrEnforcedMFAWithEmailOTP returns "system:auth.enforcedMFAWithEmailOTP" as *errors.Error
//
//
// This function is auto-generated.
//
func AuthErrEnforcedMFAWithEmailOTP(mm ...*authActionProps) *errors.Error {
var p = &authActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("OTP over email is enforced and can not be disabled", nil),
errors.Meta("type", "enforcedMFAWithEmailOTP"),
errors.Meta("resource", "system:auth"),
errors.Meta(authPropsMetaKey{}, p),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// AuthErrInvalidEmailOTP returns "system:auth.invalidEmailOTP" as *errors.Error
//
//
// This function is auto-generated.
//
func AuthErrInvalidEmailOTP(mm ...*authActionProps) *errors.Error {
var p = &authActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("invalid email OTP", nil),
errors.Meta("type", "invalidEmailOTP"),
errors.Meta("resource", "system:auth"),
errors.Meta(authPropsMetaKey{}, p),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// *********************************************************************************************************************
// *********************************************************************************************************************

View File

@@ -68,6 +68,18 @@ actions:
- action: impersonate
log: "impersonating {user}"
- action: totpConfigure
log: "time-based one-time-password for {user} configured"
- action: totpRemove
log: "time-based one-time-password for {user} removed"
- action: totpValidate
log: "time-based one-time-password for {user} validated"
- action: emailOtpVerify
log: "email one-time-password for {user} verified"
errors:
- error: invalidCredentials
message: "invalid username and password combination"
@@ -138,3 +150,39 @@ errors:
- error: notAllowedToImpersonate
message: "not allowed to impersonate this user"
severity: warning
- error: notAllowedToRemoveTOTP
message: "not allowed to remove TOTP"
severity: warning
- error: unconfiguredTOTP
message: "TOTP not configured"
severity: warning
- error: notAllowedToConfigureTOTP
message: "not allowed to configure TOTP"
severity: warning
- error: enforcedMFAWithTOTP
message: "TOTP is enforced and can not be disabled"
severity: warning
- error: invalidTOTP
message: "invalid code"
severity: warning
- error: disabledMFAWithTOTP
message: "multi factor authentication with TOTP is disabled"
severity: warning
- error: disabledMFAWithEmailOTP
message: "multi factor authentication with email OTP is disabled"
severity: warning
- error: enforcedMFAWithEmailOTP
message: "OTP over email is enforced and can not be disabled"
severity: warning
- error: invalidEmailOTP
message: "invalid code"
severity: warning

View File

@@ -23,6 +23,7 @@ type (
}
AuthNotificationService interface {
EmailOTP(ctx context.Context, lang string, emailAddress string, otp string) error
EmailConfirmation(ctx context.Context, lang string, emailAddress string, url string) error
PasswordReset(ctx context.Context, lang string, emailAddress string, url string) error
}
@@ -30,6 +31,7 @@ type (
authNotificationPayload struct {
EmailAddress string
URL string
Code string
BaseURL string
Logo htpl.URL
SignatureName string
@@ -51,6 +53,13 @@ func (svc authNotification) log(ctx context.Context, fields ...zapcore.Field) *z
return logger.AddRequestID(ctx, svc.logger).With(fields...)
}
func (svc authNotification) EmailOTP(ctx context.Context, lang string, emailAddress string, code string) error {
return svc.send(ctx, "email-otp", lang, authNotificationPayload{
EmailAddress: emailAddress,
Code: code,
})
}
func (svc authNotification) EmailConfirmation(ctx context.Context, lang string, emailAddress string, token string) error {
return svc.send(ctx, "email-confirmation", lang, authNotificationPayload{
EmailAddress: emailAddress,
@@ -124,6 +133,21 @@ func (svc authNotification) send(ctx context.Context, name, lang string, payload
}
ntf.SetBody("text/html", tmp)
case "email-otp":
// @todo move this to new template/renderer facility
ntf.SetHeader("Subject", "Login code")
bodyTpl := `{{.EmailHeaderEn}}
<h2 style="color: #568ba2;text-align: center;">Reset your password</h2>
<p>Hello,</p>
<p>Enter this code into your login form: <code>{{.Code}}</code></p>
{{.EmailFooterEn}}`
if tmp, err = svc.render(bodyTpl, payload); err != nil {
return fmt.Errorf("failed to render EmilOTP body: %w", err)
}
ntf.SetBody("text/html", tmp)
default:
return fmt.Errorf("unknown notification email template %q", name)
}

View File

@@ -55,6 +55,35 @@ type (
Providers ExternalAuthProviderSet
}
MultiFactor struct {
EmailOTP struct {
// Can users use email for MFA
Enabled bool
// Is MFA with email enforced?
Enforced bool
// Require fresh Email OTP on every client authorization
//Strict bool
Expires uint
} `kv:"email-otp"`
TOTP struct {
// Can users use TOTP for MFA
Enabled bool
// Is MFA with TOTP enforced?
Enforced bool
// Require fresh TOTP on every client authorization
//Strict bool
// TOTP issuer, defaults to "Corteza"
Issuer string
} `kv:"totp"`
} `kv:"multi-factor"`
Mail struct {
FromAddress string `kv:"from-address"`
FromName string `kv:"from-name"`

View File

@@ -40,7 +40,26 @@ type (
}
UserMeta struct {
// @todo remove, obsolete.
Avatar string `json:"avatar,omitempty"`
// User's security policy settings
SecurityPolicy struct {
// settings for multi-factor authentication
MFA struct {
// Enforce OTP on login
EnforcedEmailOTP bool `json:"enforcedEmailOTP"`
// Require OTP to be entered every time client is authorized
//StrictEmailOTP bool `json:"strictEmailOTP"`
// Is TOTP configured & enforced?
EnforcedTOTP bool `json:"enforcedTOTP"`
// Require OTP to be entered every time client is authorized
//StrictTOTP bool `json:"strictTOTP"`
} `json:"mfa"`
} `json:"securityPolicy"`
}
UserFilter struct {

1
vendor/github.com/dgryski/dgoogauth/.travis.yml generated vendored Normal file
View File

@@ -0,0 +1 @@
language: go

15
vendor/github.com/dgryski/dgoogauth/README.md generated vendored Normal file
View File

@@ -0,0 +1,15 @@
This is a Go implementation of the Google Authenticator library.
[![GoDoc](https://godoc.org/github.com/dgryski/dgoogauth?status.svg)](https://godoc.org/github.com/dgryski/dgoogauth) [![Build Status](https://travis-ci.org/dgryski/dgoogauth.png)](https://travis-ci.org/dgryski/dgoogauth)
Copyright (c) 2012 Damian Gryski <damian@gryski.com>
This code is licensed under the Apache License, version 2.0
It implements the one-time-password algorithms specified in:
* RFC 4226 (HOTP: An HMAC-Based One-Time Password Algorithm)
* RFC 6238 (TOTP: Time-Based One-Time Password Algorithm)
You can learn more about the Google Authenticator library at its project page:
* https://github.com/google/google-authenticator

199
vendor/github.com/dgryski/dgoogauth/googauth.go generated vendored Normal file
View File

@@ -0,0 +1,199 @@
/*
Package dgoogauth implements the one-time password algorithms supported by Google Authenticator
This package supports the HMAC-Based One-time Password (HOTP) algorithm
specified in RFC 4226 and the Time-based One-time Password (TOTP) algorithm
specified in RFC 6238.
*/
package dgoogauth
import (
"crypto/hmac"
"crypto/sha1"
"encoding/base32"
"encoding/binary"
"errors"
"net/url"
"sort"
"strconv"
"time"
)
// Much of this code assumes int == int64, which probably is not the case.
// ComputeCode computes the response code for a 64-bit challenge 'value' using the secret 'secret'.
// To avoid breaking compatibility with the previous API, it returns an invalid code (-1) when an error occurs,
// but does not silently ignore them (it forces a mismatch so the code will be rejected).
func ComputeCode(secret string, value int64) int {
key, err := base32.StdEncoding.DecodeString(secret)
if err != nil {
return -1
}
hash := hmac.New(sha1.New, key)
err = binary.Write(hash, binary.BigEndian, value)
if err != nil {
return -1
}
h := hash.Sum(nil)
offset := h[19] & 0x0f
truncated := binary.BigEndian.Uint32(h[offset : offset+4])
truncated &= 0x7fffffff
code := truncated % 1000000
return int(code)
}
// ErrInvalidCode indicate the supplied one-time code was not valid
var ErrInvalidCode = errors.New("invalid code")
// OTPConfig is a one-time-password configuration. This object will be modified by calls to
// Authenticate and should be saved to ensure the codes are in fact only used
// once.
type OTPConfig struct {
Secret string // 80-bit base32 encoded string of the user's secret
WindowSize int // valid range: technically 0..100 or so, but beyond 3-5 is probably bad security
HotpCounter int // the current otp counter. 0 if the user uses time-based codes instead.
DisallowReuse []int // timestamps in the current window unavailable for re-use
ScratchCodes []int // an array of 8-digit numeric codes that can be used to log in
UTC bool // use UTC for the timestamp instead of local time
}
func (c *OTPConfig) checkScratchCodes(code int) bool {
for i, v := range c.ScratchCodes {
if code == v {
// remove this code from the list of valid ones
l := len(c.ScratchCodes) - 1
c.ScratchCodes[i] = c.ScratchCodes[l] // copy last element over this element
c.ScratchCodes = c.ScratchCodes[0:l] // and trim the list length by 1
return true
}
}
return false
}
func (c *OTPConfig) checkHotpCode(code int) bool {
for i := 0; i < c.WindowSize; i++ {
if ComputeCode(c.Secret, int64(c.HotpCounter+i)) == code {
c.HotpCounter += i + 1
// We don't check for overflow here, which means you can only authenticate 2^63 times
// After that, the counter is negative and the above 'if' test will fail.
// This matches the behaviour of the PAM module.
return true
}
}
// we must always advance the counter if we tried to authenticate with it
c.HotpCounter++
return false
}
func (c *OTPConfig) checkTotpCode(t0, code int) bool {
minT := t0 - (c.WindowSize / 2)
maxT := t0 + (c.WindowSize / 2)
for t := minT; t <= maxT; t++ {
if ComputeCode(c.Secret, int64(t)) == code {
if c.DisallowReuse != nil {
for _, timeCode := range c.DisallowReuse {
if timeCode == t {
return false
}
}
// code hasn't been used before
c.DisallowReuse = append(c.DisallowReuse, t)
// remove all time codes outside of the valid window
sort.Ints(c.DisallowReuse)
min := 0
for c.DisallowReuse[min] < minT {
min++
}
// FIXME: check we don't have an off-by-one error here
c.DisallowReuse = c.DisallowReuse[min:]
}
return true
}
}
return false
}
// Authenticate a one-time-password against the given OTPConfig
// Returns true/false if the authentication was successful.
// Returns error if the password is incorrectly formatted (not a zero-padded 6 or non-zero-padded 8 digit number).
func (c *OTPConfig) Authenticate(password string) (bool, error) {
var scratch bool
switch {
case len(password) == 6 && password[0] >= '0' && password[0] <= '9':
break
case len(password) == 8 && password[0] >= '1' && password[0] <= '9':
scratch = true
break
default:
return false, ErrInvalidCode
}
code, err := strconv.Atoi(password)
if err != nil {
return false, ErrInvalidCode
}
if scratch {
return c.checkScratchCodes(code), nil
}
// we have a counter value we can use
if c.HotpCounter > 0 {
return c.checkHotpCode(code), nil
}
var t0 int
// assume we're on Time-based OTP
if c.UTC {
t0 = int(time.Now().UTC().Unix() / 30)
} else {
t0 = int(time.Now().Unix() / 30)
}
return c.checkTotpCode(t0, code), nil
}
// ProvisionURI generates a URI that can be turned into a QR code to configure
// a Google Authenticator mobile app.
func (c *OTPConfig) ProvisionURI(user string) string {
return c.ProvisionURIWithIssuer(user, "")
}
// ProvisionURIWithIssuer generates a URI that can be turned into a QR code
// to configure a Google Authenticator mobile app. It respects the recommendations
// on how to avoid conflicting accounts.
//
// See https://github.com/google/google-authenticator/wiki/Conflicting-Accounts
func (c *OTPConfig) ProvisionURIWithIssuer(user string, issuer string) string {
auth := "totp/"
q := make(url.Values)
if c.HotpCounter > 0 {
auth = "hotp/"
q.Add("counter", strconv.Itoa(c.HotpCounter))
}
q.Add("secret", c.Secret)
if issuer != "" {
q.Add("issuer", issuer)
auth += issuer + ":"
}
return "otpauth://" + auth + user + "?" + q.Encode()
}

11
vendor/modules.txt vendored
View File

@@ -1,7 +1,5 @@
# cloud.google.com/go v0.67.0
cloud.google.com/go/compute/metadata
# github.com/360EntSecGroup-Skylar/excelize/v2 v2.0.2
## explicit
# github.com/766b/chi-prometheus v0.0.0-20180509160047-46ac2b31aa30
## explicit
github.com/766b/chi-prometheus
@@ -45,6 +43,9 @@ github.com/davecgh/go-spew/spew
# github.com/dgrijalva/jwt-go v3.2.0+incompatible
## explicit
github.com/dgrijalva/jwt-go
# github.com/dgryski/dgoogauth v0.0.0-20190221195224-5a805980a5f3
## explicit
github.com/dgryski/dgoogauth
# github.com/disintegration/imaging v1.6.0
## explicit
github.com/disintegration/imaging
@@ -268,6 +269,7 @@ golang.org/x/crypto/pbkdf2
golang.org/x/crypto/scrypt
golang.org/x/crypto/ssh/terminal
# golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a
## explicit
golang.org/x/image/bmp
golang.org/x/image/ccitt
golang.org/x/image/tiff
@@ -407,3 +409,8 @@ gopkg.in/yaml.v2
# gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776
## explicit
gopkg.in/yaml.v3
# rsc.io/qr v0.2.0
## explicit
rsc.io/qr
rsc.io/qr/coding
rsc.io/qr/gf256

27
vendor/rsc.io/qr/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,27 @@
Copyright (c) 2009 The Go Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

3
vendor/rsc.io/qr/README.md generated vendored Normal file
View File

@@ -0,0 +1,3 @@
Basic QR encoder.
go get [-u] rsc.io/qr

815
vendor/rsc.io/qr/coding/qr.go generated vendored Normal file
View File

@@ -0,0 +1,815 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package coding implements low-level QR coding details.
package coding // import "rsc.io/qr/coding"
import (
"fmt"
"strconv"
"strings"
"rsc.io/qr/gf256"
)
// Field is the field for QR error correction.
var Field = gf256.NewField(0x11d, 2)
// A Version represents a QR version.
// The version specifies the size of the QR code:
// a QR code with version v has 4v+17 pixels on a side.
// Versions number from 1 to 40: the larger the version,
// the more information the code can store.
type Version int
const MinVersion = 1
const MaxVersion = 40
func (v Version) String() string {
return strconv.Itoa(int(v))
}
func (v Version) sizeClass() int {
if v <= 9 {
return 0
}
if v <= 26 {
return 1
}
return 2
}
// DataBytes returns the number of data bytes that can be
// stored in a QR code with the given version and level.
func (v Version) DataBytes(l Level) int {
vt := &vtab[v]
lev := &vt.level[l]
return vt.bytes - lev.nblock*lev.check
}
// Encoding implements a QR data encoding scheme.
// The implementations--Numeric, Alphanumeric, and String--specify
// the character set and the mapping from UTF-8 to code bits.
// The more restrictive the mode, the fewer code bits are needed.
type Encoding interface {
Check() error
Bits(v Version) int
Encode(b *Bits, v Version)
}
type Bits struct {
b []byte
nbit int
}
func (b *Bits) Reset() {
b.b = b.b[:0]
b.nbit = 0
}
func (b *Bits) Bits() int {
return b.nbit
}
func (b *Bits) Bytes() []byte {
if b.nbit%8 != 0 {
panic("fractional byte")
}
return b.b
}
func (b *Bits) Append(p []byte) {
if b.nbit%8 != 0 {
panic("fractional byte")
}
b.b = append(b.b, p...)
b.nbit += 8 * len(p)
}
func (b *Bits) Write(v uint, nbit int) {
for nbit > 0 {
n := nbit
if n > 8 {
n = 8
}
if b.nbit%8 == 0 {
b.b = append(b.b, 0)
} else {
m := -b.nbit & 7
if n > m {
n = m
}
}
b.nbit += n
sh := uint(nbit - n)
b.b[len(b.b)-1] |= uint8(v >> sh << uint(-b.nbit&7))
v -= v >> sh << sh
nbit -= n
}
}
// Num is the encoding for numeric data.
// The only valid characters are the decimal digits 0 through 9.
type Num string
func (s Num) String() string {
return fmt.Sprintf("Num(%#q)", string(s))
}
func (s Num) Check() error {
for _, c := range s {
if c < '0' || '9' < c {
return fmt.Errorf("non-numeric string %#q", string(s))
}
}
return nil
}
var numLen = [3]int{10, 12, 14}
func (s Num) Bits(v Version) int {
return 4 + numLen[v.sizeClass()] + (10*len(s)+2)/3
}
func (s Num) Encode(b *Bits, v Version) {
b.Write(1, 4)
b.Write(uint(len(s)), numLen[v.sizeClass()])
var i int
for i = 0; i+3 <= len(s); i += 3 {
w := uint(s[i]-'0')*100 + uint(s[i+1]-'0')*10 + uint(s[i+2]-'0')
b.Write(w, 10)
}
switch len(s) - i {
case 1:
w := uint(s[i] - '0')
b.Write(w, 4)
case 2:
w := uint(s[i]-'0')*10 + uint(s[i+1]-'0')
b.Write(w, 7)
}
}
// Alpha is the encoding for alphanumeric data.
// The valid characters are 0-9A-Z$%*+-./: and space.
type Alpha string
const alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:"
func (s Alpha) String() string {
return fmt.Sprintf("Alpha(%#q)", string(s))
}
func (s Alpha) Check() error {
for _, c := range s {
if strings.IndexRune(alphabet, c) < 0 {
return fmt.Errorf("non-alphanumeric string %#q", string(s))
}
}
return nil
}
var alphaLen = [3]int{9, 11, 13}
func (s Alpha) Bits(v Version) int {
return 4 + alphaLen[v.sizeClass()] + (11*len(s)+1)/2
}
func (s Alpha) Encode(b *Bits, v Version) {
b.Write(2, 4)
b.Write(uint(len(s)), alphaLen[v.sizeClass()])
var i int
for i = 0; i+2 <= len(s); i += 2 {
w := uint(strings.IndexRune(alphabet, rune(s[i])))*45 +
uint(strings.IndexRune(alphabet, rune(s[i+1])))
b.Write(w, 11)
}
if i < len(s) {
w := uint(strings.IndexRune(alphabet, rune(s[i])))
b.Write(w, 6)
}
}
// String is the encoding for 8-bit data. All bytes are valid.
type String string
func (s String) String() string {
return fmt.Sprintf("String(%#q)", string(s))
}
func (s String) Check() error {
return nil
}
var stringLen = [3]int{8, 16, 16}
func (s String) Bits(v Version) int {
return 4 + stringLen[v.sizeClass()] + 8*len(s)
}
func (s String) Encode(b *Bits, v Version) {
b.Write(4, 4)
b.Write(uint(len(s)), stringLen[v.sizeClass()])
for i := 0; i < len(s); i++ {
b.Write(uint(s[i]), 8)
}
}
// A Pixel describes a single pixel in a QR code.
type Pixel uint32
const (
Black Pixel = 1 << iota
Invert
)
func (p Pixel) Offset() uint {
return uint(p >> 6)
}
func OffsetPixel(o uint) Pixel {
return Pixel(o << 6)
}
func (r PixelRole) Pixel() Pixel {
return Pixel(r << 2)
}
func (p Pixel) Role() PixelRole {
return PixelRole(p>>2) & 15
}
func (p Pixel) String() string {
s := p.Role().String()
if p&Black != 0 {
s += "+black"
}
if p&Invert != 0 {
s += "+invert"
}
s += "+" + strconv.FormatUint(uint64(p.Offset()), 10)
return s
}
// A PixelRole describes the role of a QR pixel.
type PixelRole uint32
const (
_ PixelRole = iota
Position // position squares (large)
Alignment // alignment squares (small)
Timing // timing strip between position squares
Format // format metadata
PVersion // version pattern
Unused // unused pixel
Data // data bit
Check // error correction check bit
Extra
)
var roles = []string{
"",
"position",
"alignment",
"timing",
"format",
"pversion",
"unused",
"data",
"check",
"extra",
}
func (r PixelRole) String() string {
if Position <= r && r <= Check {
return roles[r]
}
return strconv.Itoa(int(r))
}
// A Level represents a QR error correction level.
// From least to most tolerant of errors, they are L, M, Q, H.
type Level int
const (
L Level = iota
M
Q
H
)
func (l Level) String() string {
if L <= l && l <= H {
return "LMQH"[l : l+1]
}
return strconv.Itoa(int(l))
}
// A Code is a square pixel grid.
type Code struct {
Bitmap []byte // 1 is black, 0 is white
Size int // number of pixels on a side
Stride int // number of bytes per row
}
func (c *Code) Black(x, y int) bool {
return 0 <= x && x < c.Size && 0 <= y && y < c.Size &&
c.Bitmap[y*c.Stride+x/8]&(1<<uint(7-x&7)) != 0
}
// A Mask describes a mask that is applied to the QR
// code to avoid QR artifacts being interpreted as
// alignment and timing patterns (such as the squares
// in the corners). Valid masks are integers from 0 to 7.
type Mask int
// http://www.swetake.com/qr/qr5_en.html
var mfunc = []func(int, int) bool{
func(i, j int) bool { return (i+j)%2 == 0 },
func(i, j int) bool { return i%2 == 0 },
func(i, j int) bool { return j%3 == 0 },
func(i, j int) bool { return (i+j)%3 == 0 },
func(i, j int) bool { return (i/2+j/3)%2 == 0 },
func(i, j int) bool { return i*j%2+i*j%3 == 0 },
func(i, j int) bool { return (i*j%2+i*j%3)%2 == 0 },
func(i, j int) bool { return (i*j%3+(i+j)%2)%2 == 0 },
}
func (m Mask) Invert(y, x int) bool {
if m < 0 {
return false
}
return mfunc[m](y, x)
}
// A Plan describes how to construct a QR code
// with a specific version, level, and mask.
type Plan struct {
Version Version
Level Level
Mask Mask
DataBytes int // number of data bytes
CheckBytes int // number of error correcting (checksum) bytes
Blocks int // number of data blocks
Pixel [][]Pixel // pixel map
}
// NewPlan returns a Plan for a QR code with the given
// version, level, and mask.
func NewPlan(version Version, level Level, mask Mask) (*Plan, error) {
p, err := vplan(version)
if err != nil {
return nil, err
}
if err := fplan(level, mask, p); err != nil {
return nil, err
}
if err := lplan(version, level, p); err != nil {
return nil, err
}
if err := mplan(mask, p); err != nil {
return nil, err
}
return p, nil
}
func (b *Bits) Pad(n int) {
if n < 0 {
panic("qr: invalid pad size")
}
if n <= 4 {
b.Write(0, n)
} else {
b.Write(0, 4)
n -= 4
n -= -b.Bits() & 7
b.Write(0, -b.Bits()&7)
pad := n / 8
for i := 0; i < pad; i += 2 {
b.Write(0xec, 8)
if i+1 >= pad {
break
}
b.Write(0x11, 8)
}
}
}
func (b *Bits) AddCheckBytes(v Version, l Level) {
nd := v.DataBytes(l)
if b.nbit < nd*8 {
b.Pad(nd*8 - b.nbit)
}
if b.nbit != nd*8 {
panic("qr: too much data")
}
dat := b.Bytes()
vt := &vtab[v]
lev := &vt.level[l]
db := nd / lev.nblock
extra := nd % lev.nblock
chk := make([]byte, lev.check)
rs := gf256.NewRSEncoder(Field, lev.check)
for i := 0; i < lev.nblock; i++ {
if i == lev.nblock-extra {
db++
}
rs.ECC(dat[:db], chk)
b.Append(chk)
dat = dat[db:]
}
if len(b.Bytes()) != vt.bytes {
panic("qr: internal error")
}
}
func (p *Plan) Encode(text ...Encoding) (*Code, error) {
var b Bits
for _, t := range text {
if err := t.Check(); err != nil {
return nil, err
}
t.Encode(&b, p.Version)
}
if b.Bits() > p.DataBytes*8 {
return nil, fmt.Errorf("cannot encode %d bits into %d-bit code", b.Bits(), p.DataBytes*8)
}
b.AddCheckBytes(p.Version, p.Level)
bytes := b.Bytes()
// Now we have the checksum bytes and the data bytes.
// Construct the actual code.
c := &Code{Size: len(p.Pixel), Stride: (len(p.Pixel) + 7) &^ 7}
c.Bitmap = make([]byte, c.Stride*c.Size)
crow := c.Bitmap
for _, row := range p.Pixel {
for x, pix := range row {
switch pix.Role() {
case Data, Check:
o := pix.Offset()
if bytes[o/8]&(1<<uint(7-o&7)) != 0 {
pix ^= Black
}
}
if pix&Black != 0 {
crow[x/8] |= 1 << uint(7-x&7)
}
}
crow = crow[c.Stride:]
}
return c, nil
}
// A version describes metadata associated with a version.
type version struct {
apos int
astride int
bytes int
pattern int
level [4]level
}
type level struct {
nblock int
check int
}
var vtab = []version{
{},
{100, 100, 26, 0x0, [4]level{{1, 7}, {1, 10}, {1, 13}, {1, 17}}}, // 1
{16, 100, 44, 0x0, [4]level{{1, 10}, {1, 16}, {1, 22}, {1, 28}}}, // 2
{20, 100, 70, 0x0, [4]level{{1, 15}, {1, 26}, {2, 18}, {2, 22}}}, // 3
{24, 100, 100, 0x0, [4]level{{1, 20}, {2, 18}, {2, 26}, {4, 16}}}, // 4
{28, 100, 134, 0x0, [4]level{{1, 26}, {2, 24}, {4, 18}, {4, 22}}}, // 5
{32, 100, 172, 0x0, [4]level{{2, 18}, {4, 16}, {4, 24}, {4, 28}}}, // 6
{20, 16, 196, 0x7c94, [4]level{{2, 20}, {4, 18}, {6, 18}, {5, 26}}}, // 7
{22, 18, 242, 0x85bc, [4]level{{2, 24}, {4, 22}, {6, 22}, {6, 26}}}, // 8
{24, 20, 292, 0x9a99, [4]level{{2, 30}, {5, 22}, {8, 20}, {8, 24}}}, // 9
{26, 22, 346, 0xa4d3, [4]level{{4, 18}, {5, 26}, {8, 24}, {8, 28}}}, // 10
{28, 24, 404, 0xbbf6, [4]level{{4, 20}, {5, 30}, {8, 28}, {11, 24}}}, // 11
{30, 26, 466, 0xc762, [4]level{{4, 24}, {8, 22}, {10, 26}, {11, 28}}}, // 12
{32, 28, 532, 0xd847, [4]level{{4, 26}, {9, 22}, {12, 24}, {16, 22}}}, // 13
{24, 20, 581, 0xe60d, [4]level{{4, 30}, {9, 24}, {16, 20}, {16, 24}}}, // 14
{24, 22, 655, 0xf928, [4]level{{6, 22}, {10, 24}, {12, 30}, {18, 24}}}, // 15
{24, 24, 733, 0x10b78, [4]level{{6, 24}, {10, 28}, {17, 24}, {16, 30}}}, // 16
{28, 24, 815, 0x1145d, [4]level{{6, 28}, {11, 28}, {16, 28}, {19, 28}}}, // 17
{28, 26, 901, 0x12a17, [4]level{{6, 30}, {13, 26}, {18, 28}, {21, 28}}}, // 18
{28, 28, 991, 0x13532, [4]level{{7, 28}, {14, 26}, {21, 26}, {25, 26}}}, // 19
{32, 28, 1085, 0x149a6, [4]level{{8, 28}, {16, 26}, {20, 30}, {25, 28}}}, // 20
{26, 22, 1156, 0x15683, [4]level{{8, 28}, {17, 26}, {23, 28}, {25, 30}}}, // 21
{24, 24, 1258, 0x168c9, [4]level{{9, 28}, {17, 28}, {23, 30}, {34, 24}}}, // 22
{28, 24, 1364, 0x177ec, [4]level{{9, 30}, {18, 28}, {25, 30}, {30, 30}}}, // 23
{26, 26, 1474, 0x18ec4, [4]level{{10, 30}, {20, 28}, {27, 30}, {32, 30}}}, // 24
{30, 26, 1588, 0x191e1, [4]level{{12, 26}, {21, 28}, {29, 30}, {35, 30}}}, // 25
{28, 28, 1706, 0x1afab, [4]level{{12, 28}, {23, 28}, {34, 28}, {37, 30}}}, // 26
{32, 28, 1828, 0x1b08e, [4]level{{12, 30}, {25, 28}, {34, 30}, {40, 30}}}, // 27
{24, 24, 1921, 0x1cc1a, [4]level{{13, 30}, {26, 28}, {35, 30}, {42, 30}}}, // 28
{28, 24, 2051, 0x1d33f, [4]level{{14, 30}, {28, 28}, {38, 30}, {45, 30}}}, // 29
{24, 26, 2185, 0x1ed75, [4]level{{15, 30}, {29, 28}, {40, 30}, {48, 30}}}, // 30
{28, 26, 2323, 0x1f250, [4]level{{16, 30}, {31, 28}, {43, 30}, {51, 30}}}, // 31
{32, 26, 2465, 0x209d5, [4]level{{17, 30}, {33, 28}, {45, 30}, {54, 30}}}, // 32
{28, 28, 2611, 0x216f0, [4]level{{18, 30}, {35, 28}, {48, 30}, {57, 30}}}, // 33
{32, 28, 2761, 0x228ba, [4]level{{19, 30}, {37, 28}, {51, 30}, {60, 30}}}, // 34
{28, 24, 2876, 0x2379f, [4]level{{19, 30}, {38, 28}, {53, 30}, {63, 30}}}, // 35
{22, 26, 3034, 0x24b0b, [4]level{{20, 30}, {40, 28}, {56, 30}, {66, 30}}}, // 36
{26, 26, 3196, 0x2542e, [4]level{{21, 30}, {43, 28}, {59, 30}, {70, 30}}}, // 37
{30, 26, 3362, 0x26a64, [4]level{{22, 30}, {45, 28}, {62, 30}, {74, 30}}}, // 38
{24, 28, 3532, 0x27541, [4]level{{24, 30}, {47, 28}, {65, 30}, {77, 30}}}, // 39
{28, 28, 3706, 0x28c69, [4]level{{25, 30}, {49, 28}, {68, 30}, {81, 30}}}, // 40
}
func grid(siz int) [][]Pixel {
m := make([][]Pixel, siz)
pix := make([]Pixel, siz*siz)
for i := range m {
m[i], pix = pix[:siz], pix[siz:]
}
return m
}
// vplan creates a Plan for the given version.
func vplan(v Version) (*Plan, error) {
p := &Plan{Version: v}
if v < 1 || v > 40 {
return nil, fmt.Errorf("invalid QR version %d", int(v))
}
siz := 17 + int(v)*4
m := grid(siz)
p.Pixel = m
// Timing markers (overwritten by boxes).
const ti = 6 // timing is in row/column 6 (counting from 0)
for i := range m {
p := Timing.Pixel()
if i&1 == 0 {
p |= Black
}
m[i][ti] = p
m[ti][i] = p
}
// Position boxes.
posBox(m, 0, 0)
posBox(m, siz-7, 0)
posBox(m, 0, siz-7)
// Alignment boxes.
info := &vtab[v]
for x := 4; x+5 < siz; {
for y := 4; y+5 < siz; {
// don't overwrite timing markers
if (x < 7 && y < 7) || (x < 7 && y+5 >= siz-7) || (x+5 >= siz-7 && y < 7) {
} else {
alignBox(m, x, y)
}
if y == 4 {
y = info.apos
} else {
y += info.astride
}
}
if x == 4 {
x = info.apos
} else {
x += info.astride
}
}
// Version pattern.
pat := vtab[v].pattern
if pat != 0 {
v := pat
for x := 0; x < 6; x++ {
for y := 0; y < 3; y++ {
p := PVersion.Pixel()
if v&1 != 0 {
p |= Black
}
m[siz-11+y][x] = p
m[x][siz-11+y] = p
v >>= 1
}
}
}
// One lonely black pixel
m[siz-8][8] = Unused.Pixel() | Black
return p, nil
}
// fplan adds the format pixels
func fplan(l Level, m Mask, p *Plan) error {
// Format pixels.
fb := uint32(l^1) << 13 // level: L=01, M=00, Q=11, H=10
fb |= uint32(m) << 10 // mask
const formatPoly = 0x537
rem := fb
for i := 14; i >= 10; i-- {
if rem&(1<<uint(i)) != 0 {
rem ^= formatPoly << uint(i-10)
}
}
fb |= rem
invert := uint32(0x5412)
siz := len(p.Pixel)
for i := uint(0); i < 15; i++ {
pix := Format.Pixel() + OffsetPixel(i)
if (fb>>i)&1 == 1 {
pix |= Black
}
if (invert>>i)&1 == 1 {
pix ^= Invert | Black
}
// top left
switch {
case i < 6:
p.Pixel[i][8] = pix
case i < 8:
p.Pixel[i+1][8] = pix
case i < 9:
p.Pixel[8][7] = pix
default:
p.Pixel[8][14-i] = pix
}
// bottom right
switch {
case i < 8:
p.Pixel[8][siz-1-int(i)] = pix
default:
p.Pixel[siz-1-int(14-i)][8] = pix
}
}
return nil
}
// lplan edits a version-only Plan to add information
// about the error correction levels.
func lplan(v Version, l Level, p *Plan) error {
p.Level = l
nblock := vtab[v].level[l].nblock
ne := vtab[v].level[l].check
nde := (vtab[v].bytes - ne*nblock) / nblock
extra := (vtab[v].bytes - ne*nblock) % nblock
dataBits := (nde*nblock + extra) * 8
checkBits := ne * nblock * 8
p.DataBytes = vtab[v].bytes - ne*nblock
p.CheckBytes = ne * nblock
p.Blocks = nblock
// Make data + checksum pixels.
data := make([]Pixel, dataBits)
for i := range data {
data[i] = Data.Pixel() | OffsetPixel(uint(i))
}
check := make([]Pixel, checkBits)
for i := range check {
check[i] = Check.Pixel() | OffsetPixel(uint(i+dataBits))
}
// Split into blocks.
dataList := make([][]Pixel, nblock)
checkList := make([][]Pixel, nblock)
for i := 0; i < nblock; i++ {
// The last few blocks have an extra data byte (8 pixels).
nd := nde
if i >= nblock-extra {
nd++
}
dataList[i], data = data[0:nd*8], data[nd*8:]
checkList[i], check = check[0:ne*8], check[ne*8:]
}
if len(data) != 0 || len(check) != 0 {
panic("data/check math")
}
// Build up bit sequence, taking first byte of each block,
// then second byte, and so on. Then checksums.
bits := make([]Pixel, dataBits+checkBits)
dst := bits
for i := 0; i < nde+1; i++ {
for _, b := range dataList {
if i*8 < len(b) {
copy(dst, b[i*8:(i+1)*8])
dst = dst[8:]
}
}
}
for i := 0; i < ne; i++ {
for _, b := range checkList {
if i*8 < len(b) {
copy(dst, b[i*8:(i+1)*8])
dst = dst[8:]
}
}
}
if len(dst) != 0 {
panic("dst math")
}
// Sweep up pair of columns,
// then down, assigning to right then left pixel.
// Repeat.
// See Figure 2 of http://www.pclviewer.com/rs2/qrtopology.htm
siz := len(p.Pixel)
rem := make([]Pixel, 7)
for i := range rem {
rem[i] = Extra.Pixel()
}
src := append(bits, rem...)
for x := siz; x > 0; {
for y := siz - 1; y >= 0; y-- {
if p.Pixel[y][x-1].Role() == 0 {
p.Pixel[y][x-1], src = src[0], src[1:]
}
if p.Pixel[y][x-2].Role() == 0 {
p.Pixel[y][x-2], src = src[0], src[1:]
}
}
x -= 2
if x == 7 { // vertical timing strip
x--
}
for y := 0; y < siz; y++ {
if p.Pixel[y][x-1].Role() == 0 {
p.Pixel[y][x-1], src = src[0], src[1:]
}
if p.Pixel[y][x-2].Role() == 0 {
p.Pixel[y][x-2], src = src[0], src[1:]
}
}
x -= 2
}
return nil
}
// mplan edits a version+level-only Plan to add the mask.
func mplan(m Mask, p *Plan) error {
p.Mask = m
for y, row := range p.Pixel {
for x, pix := range row {
if r := pix.Role(); (r == Data || r == Check || r == Extra) && p.Mask.Invert(y, x) {
row[x] ^= Black | Invert
}
}
}
return nil
}
// posBox draws a position (large) box at upper left x, y.
func posBox(m [][]Pixel, x, y int) {
pos := Position.Pixel()
// box
for dy := 0; dy < 7; dy++ {
for dx := 0; dx < 7; dx++ {
p := pos
if dx == 0 || dx == 6 || dy == 0 || dy == 6 || 2 <= dx && dx <= 4 && 2 <= dy && dy <= 4 {
p |= Black
}
m[y+dy][x+dx] = p
}
}
// white border
for dy := -1; dy < 8; dy++ {
if 0 <= y+dy && y+dy < len(m) {
if x > 0 {
m[y+dy][x-1] = pos
}
if x+7 < len(m) {
m[y+dy][x+7] = pos
}
}
}
for dx := -1; dx < 8; dx++ {
if 0 <= x+dx && x+dx < len(m) {
if y > 0 {
m[y-1][x+dx] = pos
}
if y+7 < len(m) {
m[y+7][x+dx] = pos
}
}
}
}
// alignBox draw an alignment (small) box at upper left x, y.
func alignBox(m [][]Pixel, x, y int) {
// box
align := Alignment.Pixel()
for dy := 0; dy < 5; dy++ {
for dx := 0; dx < 5; dx++ {
p := align
if dx == 0 || dx == 4 || dy == 0 || dy == 4 || dx == 2 && dy == 2 {
p |= Black
}
m[y+dy][x+dx] = p
}
}
}

241
vendor/rsc.io/qr/gf256/gf256.go generated vendored Normal file
View File

@@ -0,0 +1,241 @@
// Copyright 2010 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package gf256 implements arithmetic over the Galois Field GF(256).
package gf256 // import "rsc.io/qr/gf256"
import "strconv"
// A Field represents an instance of GF(256) defined by a specific polynomial.
type Field struct {
log [256]byte // log[0] is unused
exp [510]byte
}
// NewField returns a new field corresponding to the polynomial poly
// and generator α. The Reed-Solomon encoding in QR codes uses
// polynomial 0x11d with generator 2.
//
// The choice of generator α only affects the Exp and Log operations.
func NewField(poly, α int) *Field {
if poly < 0x100 || poly >= 0x200 || reducible(poly) {
panic("gf256: invalid polynomial: " + strconv.Itoa(poly))
}
var f Field
x := 1
for i := 0; i < 255; i++ {
if x == 1 && i != 0 {
panic("gf256: invalid generator " + strconv.Itoa(α) +
" for polynomial " + strconv.Itoa(poly))
}
f.exp[i] = byte(x)
f.exp[i+255] = byte(x)
f.log[x] = byte(i)
x = mul(x, α, poly)
}
f.log[0] = 255
for i := 0; i < 255; i++ {
if f.log[f.exp[i]] != byte(i) {
panic("bad log")
}
if f.log[f.exp[i+255]] != byte(i) {
panic("bad log")
}
}
for i := 1; i < 256; i++ {
if f.exp[f.log[i]] != byte(i) {
panic("bad log")
}
}
return &f
}
// nbit returns the number of significant in p.
func nbit(p int) uint {
n := uint(0)
for ; p > 0; p >>= 1 {
n++
}
return n
}
// polyDiv divides the polynomial p by q and returns the remainder.
func polyDiv(p, q int) int {
np := nbit(p)
nq := nbit(q)
for ; np >= nq; np-- {
if p&(1<<(np-1)) != 0 {
p ^= q << (np - nq)
}
}
return p
}
// mul returns the product x*y mod poly, a GF(256) multiplication.
func mul(x, y, poly int) int {
z := 0
for x > 0 {
if x&1 != 0 {
z ^= y
}
x >>= 1
y <<= 1
if y&0x100 != 0 {
y ^= poly
}
}
return z
}
// reducible reports whether p is reducible.
func reducible(p int) bool {
// Multiplying n-bit * n-bit produces (2n-1)-bit,
// so if p is reducible, one of its factors must be
// of np/2+1 bits or fewer.
np := nbit(p)
for q := 2; q < 1<<(np/2+1); q++ {
if polyDiv(p, q) == 0 {
return true
}
}
return false
}
// Add returns the sum of x and y in the field.
func (f *Field) Add(x, y byte) byte {
return x ^ y
}
// Exp returns the base-α exponential of e in the field.
// If e < 0, Exp returns 0.
func (f *Field) Exp(e int) byte {
if e < 0 {
return 0
}
return f.exp[e%255]
}
// Log returns the base-α logarithm of x in the field.
// If x == 0, Log returns -1.
func (f *Field) Log(x byte) int {
if x == 0 {
return -1
}
return int(f.log[x])
}
// Inv returns the multiplicative inverse of x in the field.
// If x == 0, Inv returns 0.
func (f *Field) Inv(x byte) byte {
if x == 0 {
return 0
}
return f.exp[255-f.log[x]]
}
// Mul returns the product of x and y in the field.
func (f *Field) Mul(x, y byte) byte {
if x == 0 || y == 0 {
return 0
}
return f.exp[int(f.log[x])+int(f.log[y])]
}
// An RSEncoder implements Reed-Solomon encoding
// over a given field using a given number of error correction bytes.
type RSEncoder struct {
f *Field
c int
gen []byte
lgen []byte
p []byte
}
func (f *Field) gen(e int) (gen, lgen []byte) {
// p = 1
p := make([]byte, e+1)
p[e] = 1
for i := 0; i < e; i++ {
// p *= (x + Exp(i))
// p[j] = p[j]*Exp(i) + p[j+1].
c := f.Exp(i)
for j := 0; j < e; j++ {
p[j] = f.Mul(p[j], c) ^ p[j+1]
}
p[e] = f.Mul(p[e], c)
}
// lp = log p.
lp := make([]byte, e+1)
for i, c := range p {
if c == 0 {
lp[i] = 255
} else {
lp[i] = byte(f.Log(c))
}
}
return p, lp
}
// NewRSEncoder returns a new Reed-Solomon encoder
// over the given field and number of error correction bytes.
func NewRSEncoder(f *Field, c int) *RSEncoder {
gen, lgen := f.gen(c)
return &RSEncoder{f: f, c: c, gen: gen, lgen: lgen}
}
// ECC writes to check the error correcting code bytes
// for data using the given Reed-Solomon parameters.
func (rs *RSEncoder) ECC(data []byte, check []byte) {
if len(check) < rs.c {
panic("gf256: invalid check byte length")
}
if rs.c == 0 {
return
}
// The check bytes are the remainder after dividing
// data padded with c zeros by the generator polynomial.
// p = data padded with c zeros.
var p []byte
n := len(data) + rs.c
if len(rs.p) >= n {
p = rs.p
} else {
p = make([]byte, n)
}
copy(p, data)
for i := len(data); i < len(p); i++ {
p[i] = 0
}
// Divide p by gen, leaving the remainder in p[len(data):].
// p[0] is the most significant term in p, and
// gen[0] is the most significant term in the generator,
// which is always 1.
// To avoid repeated work, we store various values as
// lv, not v, where lv = log[v].
f := rs.f
lgen := rs.lgen[1:]
for i := 0; i < len(data); i++ {
c := p[i]
if c == 0 {
continue
}
q := p[i+1:]
exp := f.exp[f.log[c]:]
for j, lg := range lgen {
if lg != 255 { // lgen uses 255 for log 0
q[j] ^= exp[lg]
}
}
}
copy(check, p[len(data):])
rs.p = p
}

1
vendor/rsc.io/qr/go.mod generated vendored Normal file
View File

@@ -0,0 +1 @@
module rsc.io/qr

400
vendor/rsc.io/qr/png.go generated vendored Normal file
View File

@@ -0,0 +1,400 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package qr
// PNG writer for QR codes.
import (
"bytes"
"encoding/binary"
"hash"
"hash/crc32"
)
// PNG returns a PNG image displaying the code.
//
// PNG uses a custom encoder tailored to QR codes.
// Its compressed size is about 2x away from optimal,
// but it runs about 20x faster than calling png.Encode
// on c.Image().
func (c *Code) PNG() []byte {
var p pngWriter
return p.encode(c)
}
type pngWriter struct {
tmp [16]byte
wctmp [4]byte
buf bytes.Buffer
zlib bitWriter
crc hash.Hash32
}
var pngHeader = []byte("\x89PNG\r\n\x1a\n")
func (w *pngWriter) encode(c *Code) []byte {
scale := c.Scale
siz := c.Size
w.buf.Reset()
// Header
w.buf.Write(pngHeader)
// Header block
binary.BigEndian.PutUint32(w.tmp[0:4], uint32((siz+8)*scale))
binary.BigEndian.PutUint32(w.tmp[4:8], uint32((siz+8)*scale))
w.tmp[8] = 1 // 1-bit
w.tmp[9] = 0 // gray
w.tmp[10] = 0
w.tmp[11] = 0
w.tmp[12] = 0
w.writeChunk("IHDR", w.tmp[:13])
// Comment
w.writeChunk("tEXt", comment)
// Data
w.zlib.writeCode(c)
w.writeChunk("IDAT", w.zlib.bytes.Bytes())
// End
w.writeChunk("IEND", nil)
return w.buf.Bytes()
}
var comment = []byte("Software\x00QR-PNG http://qr.swtch.com/")
func (w *pngWriter) writeChunk(name string, data []byte) {
if w.crc == nil {
w.crc = crc32.NewIEEE()
}
binary.BigEndian.PutUint32(w.wctmp[0:4], uint32(len(data)))
w.buf.Write(w.wctmp[0:4])
w.crc.Reset()
copy(w.wctmp[0:4], name)
w.buf.Write(w.wctmp[0:4])
w.crc.Write(w.wctmp[0:4])
w.buf.Write(data)
w.crc.Write(data)
crc := w.crc.Sum32()
binary.BigEndian.PutUint32(w.wctmp[0:4], crc)
w.buf.Write(w.wctmp[0:4])
}
func (b *bitWriter) writeCode(c *Code) {
const ftNone = 0
b.adler32.Reset()
b.bytes.Reset()
b.nbit = 0
scale := c.Scale
siz := c.Size
// zlib header
b.tmp[0] = 0x78
b.tmp[1] = 0
b.tmp[1] += uint8(31 - (uint16(b.tmp[0])<<8+uint16(b.tmp[1]))%31)
b.bytes.Write(b.tmp[0:2])
// Start flate block.
b.writeBits(1, 1, false) // final block
b.writeBits(1, 2, false) // compressed, fixed Huffman tables
// White border.
// First row.
b.byte(ftNone)
n := (scale*(siz+8) + 7) / 8
b.byte(255)
b.repeat(n-1, 1)
// 4*scale rows total.
b.repeat((4*scale-1)*(1+n), 1+n)
for i := 0; i < 4*scale; i++ {
b.adler32.WriteNByte(ftNone, 1)
b.adler32.WriteNByte(255, n)
}
row := make([]byte, 1+n)
for y := 0; y < siz; y++ {
row[0] = ftNone
j := 1
var z uint8
nz := 0
for x := -4; x < siz+4; x++ {
// Raw data.
for i := 0; i < scale; i++ {
z <<= 1
if !c.Black(x, y) {
z |= 1
}
if nz++; nz == 8 {
row[j] = z
j++
nz = 0
}
}
}
if j < len(row) {
row[j] = z
}
for _, z := range row {
b.byte(z)
}
// Scale-1 copies.
b.repeat((scale-1)*(1+n), 1+n)
b.adler32.WriteN(row, scale)
}
// White border.
// First row.
b.byte(ftNone)
b.byte(255)
b.repeat(n-1, 1)
// 4*scale rows total.
b.repeat((4*scale-1)*(1+n), 1+n)
for i := 0; i < 4*scale; i++ {
b.adler32.WriteNByte(ftNone, 1)
b.adler32.WriteNByte(255, n)
}
// End of block.
b.hcode(256)
b.flushBits()
// adler32
binary.BigEndian.PutUint32(b.tmp[0:], b.adler32.Sum32())
b.bytes.Write(b.tmp[0:4])
}
// A bitWriter is a write buffer for bit-oriented data like deflate.
type bitWriter struct {
bytes bytes.Buffer
bit uint32
nbit uint
tmp [4]byte
adler32 adigest
}
func (b *bitWriter) writeBits(bit uint32, nbit uint, rev bool) {
// reverse, for huffman codes
if rev {
br := uint32(0)
for i := uint(0); i < nbit; i++ {
br |= ((bit >> i) & 1) << (nbit - 1 - i)
}
bit = br
}
b.bit |= bit << b.nbit
b.nbit += nbit
for b.nbit >= 8 {
b.bytes.WriteByte(byte(b.bit))
b.bit >>= 8
b.nbit -= 8
}
}
func (b *bitWriter) flushBits() {
if b.nbit > 0 {
b.bytes.WriteByte(byte(b.bit))
b.nbit = 0
b.bit = 0
}
}
func (b *bitWriter) hcode(v int) {
/*
Lit Value Bits Codes
--------- ---- -----
0 - 143 8 00110000 through
10111111
144 - 255 9 110010000 through
111111111
256 - 279 7 0000000 through
0010111
280 - 287 8 11000000 through
11000111
*/
switch {
case v <= 143:
b.writeBits(uint32(v)+0x30, 8, true)
case v <= 255:
b.writeBits(uint32(v-144)+0x190, 9, true)
case v <= 279:
b.writeBits(uint32(v-256)+0, 7, true)
case v <= 287:
b.writeBits(uint32(v-280)+0xc0, 8, true)
default:
panic("invalid hcode")
}
}
func (b *bitWriter) byte(x byte) {
b.hcode(int(x))
}
func (b *bitWriter) codex(c int, val int, nx uint) {
b.hcode(c + val>>nx)
b.writeBits(uint32(val)&(1<<nx-1), nx, false)
}
func (b *bitWriter) repeat(n, d int) {
for ; n >= 258+3; n -= 258 {
b.repeat1(258, d)
}
if n > 258 {
// 258 < n < 258+3
b.repeat1(10, d)
b.repeat1(n-10, d)
return
}
if n < 3 {
panic("invalid flate repeat")
}
b.repeat1(n, d)
}
func (b *bitWriter) repeat1(n, d int) {
/*
Extra Extra Extra
Code Bits Length(s) Code Bits Lengths Code Bits Length(s)
---- ---- ------ ---- ---- ------- ---- ---- -------
257 0 3 267 1 15,16 277 4 67-82
258 0 4 268 1 17,18 278 4 83-98
259 0 5 269 2 19-22 279 4 99-114
260 0 6 270 2 23-26 280 4 115-130
261 0 7 271 2 27-30 281 5 131-162
262 0 8 272 2 31-34 282 5 163-194
263 0 9 273 3 35-42 283 5 195-226
264 0 10 274 3 43-50 284 5 227-257
265 1 11,12 275 3 51-58 285 0 258
266 1 13,14 276 3 59-66
*/
switch {
case n <= 10:
b.codex(257, n-3, 0)
case n <= 18:
b.codex(265, n-11, 1)
case n <= 34:
b.codex(269, n-19, 2)
case n <= 66:
b.codex(273, n-35, 3)
case n <= 130:
b.codex(277, n-67, 4)
case n <= 257:
b.codex(281, n-131, 5)
case n == 258:
b.hcode(285)
default:
panic("invalid repeat length")
}
/*
Extra Extra Extra
Code Bits Dist Code Bits Dist Code Bits Distance
---- ---- ---- ---- ---- ------ ---- ---- --------
0 0 1 10 4 33-48 20 9 1025-1536
1 0 2 11 4 49-64 21 9 1537-2048
2 0 3 12 5 65-96 22 10 2049-3072
3 0 4 13 5 97-128 23 10 3073-4096
4 1 5,6 14 6 129-192 24 11 4097-6144
5 1 7,8 15 6 193-256 25 11 6145-8192
6 2 9-12 16 7 257-384 26 12 8193-12288
7 2 13-16 17 7 385-512 27 12 12289-16384
8 3 17-24 18 8 513-768 28 13 16385-24576
9 3 25-32 19 8 769-1024 29 13 24577-32768
*/
if d <= 4 {
b.writeBits(uint32(d-1), 5, true)
} else if d <= 32768 {
nbit := uint(16)
for d <= 1<<(nbit-1) {
nbit--
}
v := uint32(d - 1)
v &^= 1 << (nbit - 1) // top bit is implicit
code := uint32(2*nbit - 2) // second bit is low bit of code
code |= v >> (nbit - 2)
v &^= 1 << (nbit - 2)
b.writeBits(code, 5, true)
// rest of bits follow
b.writeBits(uint32(v), nbit-2, false)
} else {
panic("invalid repeat distance")
}
}
func (b *bitWriter) run(v byte, n int) {
if n == 0 {
return
}
b.byte(v)
if n-1 < 3 {
for i := 0; i < n-1; i++ {
b.byte(v)
}
} else {
b.repeat(n-1, 1)
}
}
type adigest struct {
a, b uint32
}
func (d *adigest) Reset() { d.a, d.b = 1, 0 }
const amod = 65521
func aupdate(a, b uint32, pi byte, n int) (aa, bb uint32) {
// TODO(rsc): 6g doesn't do magic multiplies for b %= amod,
// only for b = b%amod.
// invariant: a, b < amod
if pi == 0 {
b += uint32(n%amod) * a
b = b % amod
return a, b
}
// n times:
// a += pi
// b += a
// is same as
// b += n*a + n*(n+1)/2*pi
// a += n*pi
m := uint32(n)
b += (m % amod) * a
b = b % amod
b += (m * (m + 1) / 2) % amod * uint32(pi)
b = b % amod
a += (m % amod) * uint32(pi)
a = a % amod
return a, b
}
func afinish(a, b uint32) uint32 {
return b<<16 | a
}
func (d *adigest) WriteN(p []byte, n int) {
for i := 0; i < n; i++ {
for _, pi := range p {
d.a, d.b = aupdate(d.a, d.b, pi, 1)
}
}
}
func (d *adigest) WriteNByte(pi byte, n int) {
d.a, d.b = aupdate(d.a, d.b, pi, n)
}
func (d *adigest) Sum32() uint32 { return afinish(d.a, d.b) }

116
vendor/rsc.io/qr/qr.go generated vendored Normal file
View File

@@ -0,0 +1,116 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
/*
Package qr encodes QR codes.
*/
package qr // import "rsc.io/qr"
import (
"errors"
"image"
"image/color"
"rsc.io/qr/coding"
)
// A Level denotes a QR error correction level.
// From least to most tolerant of errors, they are L, M, Q, H.
type Level int
const (
L Level = iota // 20% redundant
M // 38% redundant
Q // 55% redundant
H // 65% redundant
)
// Encode returns an encoding of text at the given error correction level.
func Encode(text string, level Level) (*Code, error) {
// Pick data encoding, smallest first.
// We could split the string and use different encodings
// but that seems like overkill for now.
var enc coding.Encoding
switch {
case coding.Num(text).Check() == nil:
enc = coding.Num(text)
case coding.Alpha(text).Check() == nil:
enc = coding.Alpha(text)
default:
enc = coding.String(text)
}
// Pick size.
l := coding.Level(level)
var v coding.Version
for v = coding.MinVersion; ; v++ {
if v > coding.MaxVersion {
return nil, errors.New("text too long to encode as QR")
}
if enc.Bits(v) <= v.DataBytes(l)*8 {
break
}
}
// Build and execute plan.
p, err := coding.NewPlan(v, l, 0)
if err != nil {
return nil, err
}
cc, err := p.Encode(enc)
if err != nil {
return nil, err
}
// TODO: Pick appropriate mask.
return &Code{cc.Bitmap, cc.Size, cc.Stride, 8}, nil
}
// A Code is a square pixel grid.
// It implements image.Image and direct PNG encoding.
type Code struct {
Bitmap []byte // 1 is black, 0 is white
Size int // number of pixels on a side
Stride int // number of bytes per row
Scale int // number of image pixels per QR pixel
}
// Black returns true if the pixel at (x,y) is black.
func (c *Code) Black(x, y int) bool {
return 0 <= x && x < c.Size && 0 <= y && y < c.Size &&
c.Bitmap[y*c.Stride+x/8]&(1<<uint(7-x&7)) != 0
}
// Image returns an Image displaying the code.
func (c *Code) Image() image.Image {
return &codeImage{c}
}
// codeImage implements image.Image
type codeImage struct {
*Code
}
var (
whiteColor color.Color = color.Gray{0xFF}
blackColor color.Color = color.Gray{0x00}
)
func (c *codeImage) Bounds() image.Rectangle {
d := (c.Size + 8) * c.Scale
return image.Rect(0, 0, d, d)
}
func (c *codeImage) At(x, y int) color.Color {
if c.Black(x, y) {
return blackColor
}
return whiteColor
}
func (c *codeImage) ColorModel() color.Model {
return color.GrayModel
}