Add support for MFA
This commit is contained in:
@@ -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
17
app/rand.go
Normal 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[:])))
|
||||
}
|
||||
@@ -9,4 +9,6 @@ $(function () {
|
||||
.attr('disabled', true)
|
||||
}, 50)
|
||||
})
|
||||
|
||||
$('input.mfa-code-mask').mask('000 000')
|
||||
})
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
43
auth/assets/templates/mfa-totp-disable.html.tpl
Normal file
43
auth/assets/templates/mfa-totp-disable.html.tpl
Normal 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" . }}
|
||||
89
auth/assets/templates/mfa-totp.html.tpl
Normal file
89
auth/assets/templates/mfa-totp.html.tpl
Normal 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" . }}
|
||||
104
auth/assets/templates/mfa.html.tpl
Normal file
104
auth/assets/templates/mfa.html.tpl
Normal 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" . }}
|
||||
@@ -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"
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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" . }}
|
||||
|
||||
@@ -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 }}"
|
||||
|
||||
@@ -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...)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"),
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
80
auth/handlers/handle_mfa.go
Normal file
80
auth/handlers/handle_mfa.go
Normal 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
|
||||
}
|
||||
209
auth/handlers/handle_mfa_totp.go
Normal file
209
auth/handlers/handle_mfa_totp.go
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
@@ -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
163
auth/request/auth_user.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
4
go.mod
@@ -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
33
go.sum
@@ -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=
|
||||
|
||||
@@ -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})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
// *********************************************************************************************************************
|
||||
// *********************************************************************************************************************
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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
1
vendor/github.com/dgryski/dgoogauth/.travis.yml
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
language: go
|
||||
15
vendor/github.com/dgryski/dgoogauth/README.md
generated
vendored
Normal file
15
vendor/github.com/dgryski/dgoogauth/README.md
generated
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
This is a Go implementation of the Google Authenticator library.
|
||||
|
||||
[](https://godoc.org/github.com/dgryski/dgoogauth) [](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
199
vendor/github.com/dgryski/dgoogauth/googauth.go
generated
vendored
Normal 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
11
vendor/modules.txt
vendored
@@ -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
27
vendor/rsc.io/qr/LICENSE
generated
vendored
Normal 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
3
vendor/rsc.io/qr/README.md
generated
vendored
Normal 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
815
vendor/rsc.io/qr/coding/qr.go
generated
vendored
Normal 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
241
vendor/rsc.io/qr/gf256/gf256.go
generated
vendored
Normal 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
1
vendor/rsc.io/qr/go.mod
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
module rsc.io/qr
|
||||
400
vendor/rsc.io/qr/png.go
generated
vendored
Normal file
400
vendor/rsc.io/qr/png.go
generated
vendored
Normal 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
116
vendor/rsc.io/qr/qr.go
generated
vendored
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user