diff --git a/README.md b/README.md
index 5ff92070..2c263ef5 100644
--- a/README.md
+++ b/README.md
@@ -50,7 +50,7 @@ Here is what Authelia's portal looks like
Here is the list of the main available features:
-* Several kind of second factor:
+* Several second factor methods:
* **[Security Key (U2F)](https://www.authelia.com/docs/features/2fa/security-key)** with [Yubikey].
* **[Time-based One-Time password](https://www.authelia.com/docs/features/2fa/one-time-password)**
with [Google Authenticator].
@@ -61,6 +61,7 @@ Here is the list of the main available features:
* Access restriction after too many authentication attempts.
* Fine-grained access control per subdomain, user, resource and network.
* Support of basic authentication for endpoints protected by single factor.
+* Beta support for [OpenID Connect](https://www.authelia.com/docs/configuration/identity-providers/oidc.html).
* Highly available using a remote database and Redis as a highly available KV store.
* Compatible with Kubernetes [ingress-nginx](https://github.com/kubernetes/ingress-nginx) controller out of the box.
diff --git a/cmd/authelia-scripts/cmd_bootstrap.go b/cmd/authelia-scripts/cmd_bootstrap.go
index 1c10a953..49b21af0 100644
--- a/cmd/authelia-scripts/cmd_bootstrap.go
+++ b/cmd/authelia-scripts/cmd_bootstrap.go
@@ -59,6 +59,9 @@ var hostEntries = []HostEntry{
// Kubernetes dashboard.
{Domain: "kubernetes.example.com", IP: "192.168.240.110"},
+ // OIDC tester app
+ {Domain: "oidc.example.com", IP: "192.168.240.100"},
+ {Domain: "oidc-public.example.com", IP: "192.168.240.100"},
}
func runCommand(cmd string, args ...string) {
diff --git a/cmd/authelia-scripts/main.go b/cmd/authelia-scripts/main.go
index ec1a29d0..c3c3d05d 100755
--- a/cmd/authelia-scripts/main.go
+++ b/cmd/authelia-scripts/main.go
@@ -59,7 +59,7 @@ var Commands = []AutheliaCommandDefinition{
},
{
Name: "suites",
- Short: "Compute hash of a password for creating a file-based users database",
+ Short: "Commands related to suites management",
SubCommands: CobraCommands{
SuitesTestCmd,
SuitesListCmd,
@@ -135,7 +135,7 @@ func main() {
cobraCommands = append(cobraCommands, command)
}
- cobraCommands = append(cobraCommands, commands.HashPasswordCmd)
+ cobraCommands = append(cobraCommands, commands.HashPasswordCmd, commands.CertificatesCmd, commands.RSACmd)
rootCmd.PersistentFlags().StringVar(&logLevel, "log-level", "info", "Set the log level for the command")
rootCmd.AddCommand(cobraCommands...)
diff --git a/cmd/authelia-suites/main.go b/cmd/authelia-suites/main.go
index 26515777..271df298 100644
--- a/cmd/authelia-suites/main.go
+++ b/cmd/authelia-suites/main.go
@@ -83,7 +83,7 @@ func setupSuite(cmd *cobra.Command, args []string) {
suiteResourcePath := cwd + "/internal/suites/" + suiteName
- exist, err := utils.FileExists(suiteResourcePath)
+ exist, err := utils.PathExists(suiteResourcePath)
if err != nil {
log.Fatal(err)
diff --git a/cmd/authelia/main.go b/cmd/authelia/main.go
index 58a92592..8e125420 100644
--- a/cmd/authelia/main.go
+++ b/cmd/authelia/main.go
@@ -14,6 +14,7 @@ import (
"github.com/authelia/authelia/internal/logging"
"github.com/authelia/authelia/internal/middlewares"
"github.com/authelia/authelia/internal/notification"
+ "github.com/authelia/authelia/internal/oidc"
"github.com/authelia/authelia/internal/regulation"
"github.com/authelia/authelia/internal/server"
"github.com/authelia/authelia/internal/session"
@@ -117,15 +118,22 @@ func startServer() {
authorizer := authorization.NewAuthorizer(config.AccessControl)
sessionProvider := session.NewProvider(config.Session, autheliaCertPool)
regulator := regulation.NewRegulator(config.Regulation, storageProvider, clock)
+ oidcProvider, err := oidc.NewOpenIDConnectProvider(config.IdentityProviders.OIDC)
+
+ if err != nil {
+ panic(err)
+ }
providers := middlewares.Providers{
Authorizer: authorizer,
UserProvider: userProvider,
Regulator: regulator,
+ OpenIDConnect: oidcProvider,
StorageProvider: storageProvider,
Notifier: notifier,
SessionProvider: sessionProvider,
}
+
server.StartServer(*config, providers)
}
@@ -149,7 +157,8 @@ func main() {
}
rootCmd.AddCommand(versionCmd, commands.HashPasswordCmd,
- commands.ValidateConfigCmd, commands.CertificatesCmd)
+ commands.ValidateConfigCmd, commands.CertificatesCmd,
+ commands.RSACmd)
if err := rootCmd.Execute(); err != nil {
logger.Fatal(err)
diff --git a/config.template.yml b/config.template.yml
index 154da0aa..b279bb11 100644
--- a/config.template.yml
+++ b/config.template.yml
@@ -553,4 +553,62 @@ notifier:
# sender: admin@example.com
# host: smtp.gmail.com
# port: 587
+
+##
+## Identity Providers
+##
+# identity_providers:
+
+ ##
+ ## OpenID Connect (Identity Provider)
+ ##
+ ## It's recommended you read the documentation before configuration of this section:
+ ## https://www.authelia.com/docs/configuration/identity-providers/oidc.html
+ # oidc:
+ ## The hmac_secret is used to sign OAuth2 tokens (authorization code, access tokens and refresh tokens).
+ ## HMAC Secret can also be set using a secret: https://www.authelia.com/docs/configuration/secrets.html
+ # hmac_secret: this_is_a_secret_abc123abc123abc
+
+ ## The issuer_private_key is used to sign the JWT forged by OpenID Connect.
+ ## Issuer Private Key can also be set using a secret: https://docs.authelia.com/configuration/secrets.html
+ # issuer_private_key: |
+ # --- KEY START
+ # --- KEY END
+
+ ## Clients is a list of known clients and their configuration.
+ # clients:
+ # -
+ ## The ID is the OpenID Connect ClientID which is used to link an application to a configuration.
+ # id: myapp
+
+ ## The description to show to users when they end up on the consent screen. Defaults to the ID above.
+ # description: My Application
+
+ ## The client secret is a shared secret between Authelia and the consumer of this client.
+ # secret: this_is_a_secret
+
+ ## The policy to require for this client; one_factor or two_factor.
+ # authorization_policy: two_factor
+
+ ## Redirect URI's specifies a list of valid case-sensitive callbacks for this client.
+ # redirect_uris:
+ # - https://oidc.example.com:8080/oauth2/callback
+
+ ## Scopes defines the valid scopes this client can request
+ # scopes:
+ # - openid
+ # - groups
+ # - email
+ # - profile
+
+ ## Grant Types configures which grants this client can obtain.
+ ## It's not recommended to define this unless you know what you're doing.
+ # grant_types:
+ # - refresh_token
+ # - "authorization_code
+
+ ## Response Types configures which responses this client can be sent.
+ ## It's not recommended to define this unless you know what you're doing.
+ # response_types:
+ # - code
...
diff --git a/docs/_sass/custom/custom.scss b/docs/_sass/custom/custom.scss
index 0e9400ab..5a43d728 100644
--- a/docs/_sass/custom/custom.scss
+++ b/docs/_sass/custom/custom.scss
@@ -1,3 +1,10 @@
.label.label-config {
text-transform: none;
+}
+.tbl-header {
+ font-weight: bold;
+ text-align: center;
+}
+.tbl-beta-stage {
+ border-bottom-width: 3px !important;
}
\ No newline at end of file
diff --git a/docs/configuration/identity-providers/index.md b/docs/configuration/identity-providers/index.md
new file mode 100644
index 00000000..b5a62d94
--- /dev/null
+++ b/docs/configuration/identity-providers/index.md
@@ -0,0 +1,12 @@
+---
+layout: default
+title: Identity Providers
+parent: Configuration
+nav_order: 12
+has_children: true
+---
+
+# Identity Providers
+
+This section covers configuration of the identity server characteristics of Authelia. Currently the only identity server
+supported is OpenID Connect.
diff --git a/docs/configuration/identity-providers/oidc.md b/docs/configuration/identity-providers/oidc.md
new file mode 100644
index 00000000..b4431a95
--- /dev/null
+++ b/docs/configuration/identity-providers/oidc.md
@@ -0,0 +1,225 @@
+---
+layout: default
+title: OpenID Connect
+parent: Identity Providers
+grand_parent: Configuration
+nav_order: 2
+---
+
+# OpenID Connect
+
+**Authelia** currently supports the [OpenID Connect] OP role as a [beta](#beta) feature. The OP role is the
+[OpenID Connect] Provider role, not the Relaying Party or RP role. This means other applications that implement the
+[OpenID Connect] RP role can use Authelia as an authentication and authorization backend similar to how you may use
+social media or development platforms for login.
+
+The Relaying Party role is the role which allows an application to use GitHub, Google, or other [OpenID Connect]
+providers for authentication and authorization. We do not intend to support this functionality at this moment in time.
+
+## Beta
+
+We have decided to implement [OpenID Connect] as a beta feature, it's suggested you only utilize it for testing and
+providing feedback, and should take caution in relying on it in production. [OpenID Connect] and it's related endpoints
+are not enabled by default unless you specifically configure the [OpenID Connect] section.
+
+The beta will be broken up into stages. Each stage will bring additional features. The following table is a *rough* plan
+for which stage will have each feature, and may evolve over time:
+
+
+
+
+
+
+
+
+
+
+
+ User Consent
+
+
+ Authorization Code Flow
+
+
+ OpenID Connect Discovery
+
+
+ RS256 Signature Strategy
+
+
+ Per Client Scope/Grant Type/Response Type Restriction
+
+
+ Per Client Authorization Policy (1FA/2FA)
+
+
+ Per Client List of Valid Redirection URI's
+
+
+
+ Token Storage
+
+
+ Audit Storage
+
+
+
+ Back-Channel Logout
+
+
+ Deny Refresh on Session Expiration
+
+
+ Signing Key Rotation Policy
+
+
+ Client Secrets Hashed in Configuration
+
+
+
+ General Availability after previous stages are vetted for bug fixes
+
+
+
+ List of other features that may be implemented
+
+
+ Front-Channel Logout 2
+
+
+
+
+*1 this stage has not been implemented as of yet*
+
+*2 this individual feature has not been implemented as of yet*
+
+## Configuration
+
+```yaml
+identity_providers:
+ oidc:
+ hmac_secret: this_is_a_secret_abc123abc123abc
+ issuer_private_key: |
+ --- KEY START
+ --- KEY END
+ clients:
+ - id: myapp
+ description: My Application
+ secret: this_is_a_secret
+ authorization_policy: two_factor
+ redirect_uris:
+ - https://oidc.example.com:8080/oauth2/callback
+ scopes:
+ - openid
+ - groups
+ - email
+ - profile
+ grant_types:
+ - refresh_token
+ - authorization_code
+ response_types:
+ - code
+```
+
+## Options
+
+### hmac_secret
+
+The HMAC secret used to sign the [OpenID Connect] JWT's. The provided string is hashed to a SHA256 byte string for
+the purpose of meeting the required format.
+
+Can also be defined using a [secret](../secrets.md) which is the recommended for containerized deployments.
+
+### issuer_private_key
+
+The private key in DER base64 encoded PEM format used to encrypt the [OpenID Connect] JWT's.
+
+Can also be defined using a [secret](../secrets.md) which is the recommended for containerized deployments.
+
+### clients
+
+A list of clients to configure. The options for each client are described below.
+
+#### id
+
+The Client ID for this client. Must be configured in the application consuming this client.
+
+#### description
+
+A friendly description for this client shown in the UI. This defaults to the same as the ID.
+
+#### secret
+
+The shared secret between Authelia and the application consuming this client. Currently this is stored in plain text.
+
+#### authorization_policy
+
+The authorization policy for this client. Either `one_factor` or `two_factor`.
+
+#### redirect_uris
+
+A list of valid callback URL's this client will redirect to. All other callbacks will be considered unsafe. The URL's
+are case-sensitive.
+
+#### scopes
+
+A list of scopes to allow this client to consume. See [scope definitions](#scope-definitions) for more information.
+
+#### grant_types
+
+A list of grant types this client can return. It is recommended that this isn't configured at this time unless you know
+what you're doing.
+
+#### response_types
+
+A list of response types this client can return. It is recommended that this isn't configured at this time unless you
+know what you're doing.
+
+## Scope Definitions
+
+### openid
+
+This is the default scope for openid. This field is forced on every client by the configuration
+validation that Authelia does.
+
+|JWT Field|JWT Type |Authelia Attribute|Description |
+|:-------:|:-----------:|:----------------:|:--------------------------------------:|
+|sub |string |Username |The username the user used to login with|
+|scope |string |scopes |Granted scopes (space delimited) |
+|scp |array[string]|scopes |Granted scopes |
+|iss |string |hostname |The issuer name, determined by URL |
+|at_hash |string |_N/A_ |Access Token Hash |
+|auth_time|number |_N/A_ |Authorize Time |
+|aud |array[string]|_N/A_ |Audience |
+|exp |number |_N/A_ |Expires |
+|iat |number |_N/A_ |Issued At |
+|rat |number |_N/A_ |Requested At |
+|jti |string(uuid) |_N/A_ |JWT Identifier |
+
+### groups
+
+This scope includes the groups the authentication backend reports the user is a member of in the token.
+
+|JWT Field|JWT Type |Authelia Attribute|Description |
+|:-------:|:-----------:|:----------------:|:--------------------:|
+|groups |array[string]|Groups |The users display name|
+
+### email
+
+This scope includes the email information the authentication backend reports about the user in the token.
+
+|JWT Field |JWT Type|Authelia Attribute|Description |
+|:------------:|:------:|:----------------:|:-------------------------------------------------------:|
+|email |string |email[0] |The first email in the list of emails |
+|email_verified|bool |_N/A_ |If the email is verified, assumed true for the time being|
+
+### profile
+
+This scope includes the profile information the authentication backend reports about the user in the token.
+
+|JWT Field|JWT Type|Authelia Attribute|Description |
+|:-------:|:------:|:----------------:|:--------------------:|
+|name |string | display_name |The users display name|
+
+
+[OpenID Connect]: https://openid.net/connect/
\ No newline at end of file
diff --git a/docs/index.md b/docs/index.md
index f8606080..3da01405 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -40,7 +40,7 @@ so that you can test it in minutes. Let's begin with the
## However, Authelia...
-* is not an OAuth or OpenID Connect provider yet (planned in the [roadmap](./roadmap.md))
+* [OpenID Connect](./configuration/identity-providers/oidc.md) is still in preview.
* is not a SAML provider yet.
* does not support authentication against an OAuth or OpenID Connect provider yet.
* does not support authentication against a SAML provider yet.
diff --git a/docs/roadmap.md b/docs/roadmap.md
index f34b97c5..5bf4c887 100644
--- a/docs/roadmap.md
+++ b/docs/roadmap.md
@@ -14,7 +14,9 @@ ideas and plans with you.
Below are the prioritised roadmap items:
-1. [Authelia acts as an OpenID Connect Provider](https://github.com/authelia/authelia/issues/189). This is a high
+1. **[In Preview](./configuration/identity-providers/oidc.md)** *this roadmap item is in preview status, more
+ information can be found in the docs*.
+ [Authelia acts as an OpenID Connect Provider](https://github.com/authelia/authelia/issues/189). This is a high
priority because currently the only way to pass authentication information back to the protected app is through the
use of HTTP headers as described
[here](https://www.authelia.com/docs/deployment/supported-proxies/#how-can-the-backend-be-aware-of-the-authenticated-users)
diff --git a/go.mod b/go.mod
index f3f250e9..f79d8b8a 100644
--- a/go.mod
+++ b/go.mod
@@ -17,9 +17,9 @@ require (
github.com/go-sql-driver/mysql v1.6.0
github.com/golang/mock v1.5.0
github.com/jackc/pgx/v4 v4.11.0
- github.com/mattn/go-sqlite3 v1.14.7
+ github.com/mattn/go-sqlite3 v2.0.3+incompatible
+ github.com/ory/fosite v0.39.0
github.com/otiai10/copy v1.5.1
- github.com/pelletier/go-toml v1.4.0 // indirect
github.com/pquerna/otp v1.3.0
github.com/simia-tech/crypt v0.5.0
github.com/sirupsen/logrus v1.8.1
@@ -30,5 +30,6 @@ require (
github.com/tstranex/u2f v1.0.0
github.com/valyala/fasthttp v1.24.0
golang.org/x/text v0.3.6
+ gopkg.in/square/go-jose.v2 v2.5.1
gopkg.in/yaml.v2 v2.4.0
)
diff --git a/go.sum b/go.sum
index 0ddc121f..48f266fd 100644
--- a/go.sum
+++ b/go.sum
@@ -1,3 +1,4 @@
+bazil.org/fuse v0.0.0-20160811212531-371fbbdaa898/go.mod h1:Xbm+BRKSBEpa4q4hTSxohYNQpsxXPbPry4JJWOB3LB8=
cloud.google.com/go v0.26.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=
@@ -12,6 +13,7 @@ cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqCl
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
+github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c h1:/IBSNwUN8+eKzUzbJPqhK839ygXJ82sde8x3ogr6R28=
github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
@@ -20,13 +22,22 @@ github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802 h1:1BDTz0u9nC3//pOC
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/BurntSushi/xgbutil v0.0.0-20160919175755-f7c97cef3b4e h1:4ZrkT/RzpnROylmoQL57iVUL57wGKTR5O6KpVnbm2tA=
github.com/BurntSushi/xgbutil v0.0.0-20160919175755-f7c97cef3b4e/go.mod h1:uw9h2sd4WWHOPdJ13MQpwK5qYWKYDumDqxWWIknEQ+k=
+github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
+github.com/DataDog/datadog-go v4.0.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
github.com/Gurpartap/logrus-stack v0.0.0-20170710170904-89c00d8a28f4 h1:vdT7QwBhJJEVNFMBNhRSFDRCB6O16T28VhvqRgqFyn8=
github.com/Gurpartap/logrus-stack v0.0.0-20170710170904-89c00d8a28f4/go.mod h1:SvXOG8ElV28oAiG9zv91SDe5+9PfIr7PPccpr8YyXNs=
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
+github.com/Masterminds/semver v1.4.2 h1:WBLTQ37jOCzSLtXNdoo8bNM8876KhNqOKvrlGITgsTc=
+github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
+github.com/Masterminds/semver/v3 v3.0.3/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc=
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
+github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA=
+github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
+github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk=
+github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
@@ -34,6 +45,8 @@ github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/
github.com/Workiva/go-datastructures v1.0.53 h1:J6Y/52yX10Xc5JjXmGtWoSSxs3mZnGSaq37xZZh7Yig=
github.com/Workiva/go-datastructures v1.0.53/go.mod h1:1yZL+zfsztete+ePzZz/Zb1/t5BnDuE2Ya2MMGhzP6A=
github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c=
+github.com/ajg/form v0.0.0-20160822230020-523a5da1a92f/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
+github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
@@ -43,16 +56,22 @@ github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
+github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
+github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A=
+github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
+github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg=
github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef h1:46PFijGLmAjMPwCCCo7Jf0W6f9slllCkkv7vyc1yOSg=
github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU=
+github.com/aws/aws-sdk-go v1.23.19/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
+github.com/aws/aws-xray-sdk-go v0.9.4/go.mod h1:XtMKdBQfpVut+tJEwI7+dJFRxxRdxHDyVNp2tHXRq04=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
@@ -60,24 +79,38 @@ github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kB
github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
+github.com/bmatcuk/doublestar/v2 v2.0.3/go.mod h1:QMmcs3H2AUQICWhfzLXz+IYln8lRQmTZRptLie8RgRw=
+github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI=
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA=
github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ=
+github.com/cenkalti/backoff v2.1.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
+github.com/cenkalti/backoff/v3 v3.0.0/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/cihub/seelog v0.0.0-20170130134532-f561c5e57575/go.mod h1:9d6lWj8KzO/fd/NrVaLscBKmPigpZpn5YawRPw+e3Yo=
github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
+github.com/cockroachdb/cockroach-go v0.0.0-20181001143604-e0a95dfd547c/go.mod h1:XGLbWH/ujMcbPbhZq52Nv6UrCghb1yGn//133kEsvDk=
+github.com/cockroachdb/cockroach-go v0.0.0-20190925194419-606b3d062051/go.mod h1:XGLbWH/ujMcbPbhZq52Nv6UrCghb1yGn//133kEsvDk=
+github.com/cockroachdb/cockroach-go v0.0.0-20200312223839-f565e4789405/go.mod h1:XGLbWH/ujMcbPbhZq52Nv6UrCghb1yGn//133kEsvDk=
github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8=
github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI=
+github.com/codegangsta/negroni v1.0.0/go.mod h1:v0y3T5G7Y1UlFfyxFn/QLRU4a2EuNau2iZY63YTKWo0=
+github.com/containerd/continuity v0.0.0-20181203112020-004b46473808/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
+github.com/containerd/continuity v0.0.0-20190827140505-75bee3e2ccb6/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
+github.com/containerd/continuity v0.0.0-20200107194136-26c1120b8d41/go.mod h1:Dq467ZllaHgAtVp4p1xUQWBrFXR9s/wyoTpG8zOJGkY=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
+github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
+github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
@@ -85,26 +118,42 @@ github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
+github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
+github.com/cucumber/godog v0.8.1/go.mod h1:vSh3r/lM+psC1BPXvdkSEuNjmXfpVqrMGYAElF6hxnA=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/deckarep/golang-set v1.7.1 h1:SCQV0S6gTtp6itiFrTqI+pfmJ4LN85S1YzhDf9rTHJQ=
github.com/deckarep/golang-set v1.7.1/go.mod h1:93vsz/8Wt4joVM7c2AVqh+YRMiUSc14yDtF28KmMOgQ=
+github.com/dgraph-io/ristretto v0.0.1/go.mod h1:T40EBc7CJke8TkpiYfGGKAeFjSaxuFXhuXRyumBd6RE=
+github.com/dgraph-io/ristretto v0.0.2/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E=
+github.com/dgraph-io/ristretto v0.0.3 h1:jh22xisGBjrEVnRZ1DVTpBVQm0Xndu8sMl0CWDzSIBI=
+github.com/dgraph-io/ristretto v0.0.3/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E=
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/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA=
+github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
+github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
+github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
+github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/duosecurity/duo_api_golang v0.0.0-20201112143038-0e07e9f869e3 h1:7/i/g2rlBeX1DHg5xTrR2hiFi87ZrqRWV3eLZUApjdI=
github.com/duosecurity/duo_api_golang v0.0.0-20201112143038-0e07e9f869e3/go.mod h1:jdoEJUIrTIxN7nNTwwqA3TBNcSM+W1lrWM6OXVhjbG8=
github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
+github.com/dustin/go-humanize v0.0.0-20180713052910-9f541cc9db5d/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
+github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
+github.com/elastic/go-sysinfo v1.1.1/go.mod h1:i1ZYdU10oLNfRzq4vq62BEwD2fH8KaWh6eh0ikPT9F0=
+github.com/elastic/go-windows v1.0.0/go.mod h1:TsU0Nrp7/y3+VwE82FoZF8gC/XFg/Elz6CcloAxnPgU=
+github.com/elazarl/goproxy v0.0.0-20181003060214-f58a169a71a5/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
@@ -115,6 +164,11 @@ github.com/fasthttp/router v1.3.12/go.mod h1:u+cTxz7conAkhUqSYx0em5kfYwDaslNehUy
github.com/fasthttp/session/v2 v2.3.2 h1:QtGPJGqcNriXN+dqz20z0yp3Kg9H28emODZ1ILcSQpE=
github.com/fasthttp/session/v2 v2.3.2/go.mod h1:Fb8sNPFfYLBFHZWZbj1wjODHwe7/PNeuXsmTIkHAomQ=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
+github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
+github.com/fatih/structs v1.0.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
+github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
+github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
+github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4=
github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
@@ -123,6 +177,7 @@ github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-asn1-ber/asn1-ber v1.5.1 h1:pDbRAunXzIUXfx4CB2QJFv5IuPiuoW+sWvr/Us009o8=
github.com/go-asn1-ber/asn1-ber v1.5.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
+github.com/go-bindata/go-bindata v3.1.1+incompatible/go.mod h1:xK8Dsgwmeed+BBsSy2XTopBn/8uK2HWuGSnA11C3Joo=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
@@ -135,15 +190,251 @@ github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG
github.com/go-redis/redis/v8 v8.3.4 h1:ZF7juZS2wzxloqMKslTutWJ05IQrnchCSk1HD4d4Vbs=
github.com/go-redis/redis/v8 v8.3.4/go.mod h1:jszGxBCez8QA1HWSmQxJO9Y82kNibbUmeYhKWrBejTU=
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
+github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
+github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
+github.com/gobuffalo/attrs v0.1.0/go.mod h1:fmNpaWyHM0tRm8gCZWKx8yY9fvaNLo2PyzBNSrBZ5Hw=
+github.com/gobuffalo/buffalo v0.12.8-0.20181004233540-fac9bb505aa8/go.mod h1:sLyT7/dceRXJUxSsE813JTQtA3Eb1vjxWfo/N//vXIY=
+github.com/gobuffalo/buffalo v0.13.0/go.mod h1:Mjn1Ba9wpIbpbrD+lIDMy99pQ0H0LiddMIIDGse7qT4=
+github.com/gobuffalo/buffalo-plugins v1.0.2/go.mod h1:pOp/uF7X3IShFHyobahTkTLZaeUXwb0GrUTb9ngJWTs=
+github.com/gobuffalo/buffalo-plugins v1.0.4/go.mod h1:pWS1vjtQ6uD17MVFWf7i3zfThrEKWlI5+PYLw/NaDB4=
+github.com/gobuffalo/buffalo-plugins v1.4.3/go.mod h1:uCzTY0woez4nDMdQjkcOYKanngeUVRO2HZi7ezmAjWY=
+github.com/gobuffalo/buffalo-plugins v1.5.1/go.mod h1:jbmwSZK5+PiAP9cC09VQOrGMZFCa/P0UMlIS3O12r5w=
+github.com/gobuffalo/buffalo-plugins v1.6.4/go.mod h1:/+N1aophkA2jZ1ifB2O3Y9yGwu6gKOVMtUmJnbg+OZI=
+github.com/gobuffalo/buffalo-plugins v1.6.5/go.mod h1:0HVkbgrVs/MnPZ/FOseDMVanCTm2RNcdM0PuXcL1NNI=
+github.com/gobuffalo/buffalo-plugins v1.6.7/go.mod h1:ZGZRkzz2PiKWHs0z7QsPBOTo2EpcGRArMEym6ghKYgk=
+github.com/gobuffalo/buffalo-plugins v1.6.9/go.mod h1:yYlYTrPdMCz+6/+UaXg5Jm4gN3xhsvsQ2ygVatZV5vw=
+github.com/gobuffalo/buffalo-plugins v1.6.11/go.mod h1:eAA6xJIL8OuynJZ8amXjRmHND6YiusVAaJdHDN1Lu8Q=
+github.com/gobuffalo/buffalo-plugins v1.8.2/go.mod h1:9te6/VjEQ7pKp7lXlDIMqzxgGpjlKoAcAANdCgoR960=
+github.com/gobuffalo/buffalo-plugins v1.8.3/go.mod h1:IAWq6vjZJVXebIq2qGTLOdlXzmpyTZ5iJG5b59fza5U=
+github.com/gobuffalo/buffalo-plugins v1.9.4/go.mod h1:grCV6DGsQlVzQwk6XdgcL3ZPgLm9BVxlBmXPMF8oBHI=
+github.com/gobuffalo/buffalo-plugins v1.10.0/go.mod h1:4osg8d9s60txLuGwXnqH+RCjPHj9K466cDFRl3PErHI=
+github.com/gobuffalo/buffalo-plugins v1.11.0/go.mod h1:rtIvAYRjYibgmWhnjKmo7OadtnxuMG5ZQLr25ozAzjg=
+github.com/gobuffalo/buffalo-plugins v1.15.0/go.mod h1:BqSx01nwgKUQr/MArXzFkSD0QvdJidiky1OKgyfgrK8=
+github.com/gobuffalo/buffalo-pop v1.0.5/go.mod h1:Fw/LfFDnSmB/vvQXPvcXEjzP98Tc+AudyNWUBWKCwQ8=
+github.com/gobuffalo/envy v1.6.4/go.mod h1:Abh+Jfw475/NWtYMEt+hnJWRiC8INKWibIMyNt1w2Mc=
+github.com/gobuffalo/envy v1.6.5/go.mod h1:N+GkhhZ/93bGZc6ZKhJLP6+m+tCNPKwgSpH9kaifseQ=
+github.com/gobuffalo/envy v1.6.6/go.mod h1:N+GkhhZ/93bGZc6ZKhJLP6+m+tCNPKwgSpH9kaifseQ=
+github.com/gobuffalo/envy v1.6.7/go.mod h1:N+GkhhZ/93bGZc6ZKhJLP6+m+tCNPKwgSpH9kaifseQ=
+github.com/gobuffalo/envy v1.6.8/go.mod h1:N+GkhhZ/93bGZc6ZKhJLP6+m+tCNPKwgSpH9kaifseQ=
+github.com/gobuffalo/envy v1.6.9/go.mod h1:N+GkhhZ/93bGZc6ZKhJLP6+m+tCNPKwgSpH9kaifseQ=
+github.com/gobuffalo/envy v1.6.10/go.mod h1:X0CFllQjTV5ogsnUrg+Oks2yTI+PU2dGYBJOEI2D1Uo=
+github.com/gobuffalo/envy v1.6.11/go.mod h1:Fiq52W7nrHGDggFPhn2ZCcHw4u/rqXkqo+i7FB6EAcg=
+github.com/gobuffalo/envy v1.6.12/go.mod h1:qJNrJhKkZpEW0glh5xP2syQHH5kgdmgsKss2Kk8PTP0=
+github.com/gobuffalo/envy v1.6.15/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI=
+github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI=
+github.com/gobuffalo/envy v1.7.1/go.mod h1:FurDp9+EDPE4aIUS3ZLyD+7/9fpx7YRt/ukY6jIHf0w=
+github.com/gobuffalo/envy v1.8.1/go.mod h1:FurDp9+EDPE4aIUS3ZLyD+7/9fpx7YRt/ukY6jIHf0w=
+github.com/gobuffalo/envy v1.9.0/go.mod h1:FurDp9+EDPE4aIUS3ZLyD+7/9fpx7YRt/ukY6jIHf0w=
+github.com/gobuffalo/events v1.0.3/go.mod h1:Txo8WmqScapa7zimEQIwgiJBvMECMe9gJjsKNPN3uZw=
+github.com/gobuffalo/events v1.0.7/go.mod h1:z8txf6H9jWhQ5Scr7YPLWg/cgXBRj8Q4uYI+rsVCCSQ=
+github.com/gobuffalo/events v1.0.8/go.mod h1:A5KyqT1sA+3GJiBE4QKZibse9mtOcI9nw8gGrDdqYGs=
+github.com/gobuffalo/events v1.1.3/go.mod h1:9yPGWYv11GENtzrIRApwQRMYSbUgCsZ1w6R503fCfrk=
+github.com/gobuffalo/events v1.1.4/go.mod h1:09/YRRgZHEOts5Isov+g9X2xajxdvOAcUuAHIX/O//A=
+github.com/gobuffalo/events v1.1.5/go.mod h1:3YUSzgHfYctSjEjLCWbkXP6djH2M+MLaVRzb4ymbAK0=
+github.com/gobuffalo/events v1.1.7/go.mod h1:6fGqxH2ing5XMb3EYRq9LEkVlyPGs4oO/eLzh+S8CxY=
+github.com/gobuffalo/events v1.1.8/go.mod h1:UFy+W6X6VbCWS8k2iT81HYX65dMtiuVycMy04cplt/8=
+github.com/gobuffalo/events v1.1.9/go.mod h1:/0nf8lMtP5TkgNbzYxR6Bl4GzBy5s5TebgNTdRfRbPM=
+github.com/gobuffalo/events v1.3.1/go.mod h1:9JOkQVoyRtailYVE/JJ2ZQ/6i4gTjM5t2HsZK4C1cSA=
+github.com/gobuffalo/events v1.4.1/go.mod h1:SjXgWKpeSuvQDvGhgMz5IXx3Czu+IbL+XPLR41NvVQY=
+github.com/gobuffalo/fizz v1.0.12/go.mod h1:C0sltPxpYK8Ftvf64kbsQa2yiCZY4RZviurNxXdAKwc=
+github.com/gobuffalo/fizz v1.9.8/go.mod h1:w1FEn1yKNVCc49KnADGyYGRPH7jFON3ak4Bj1yUudHo=
+github.com/gobuffalo/flect v0.0.0-20180907193754-dc14d8acaf9f/go.mod h1:rCiQgmAE4axgBNl3jZWzS5rETRYTGOsrixTRaCPzNdA=
+github.com/gobuffalo/flect v0.0.0-20181002182613-4571df4b1daf/go.mod h1:rCiQgmAE4axgBNl3jZWzS5rETRYTGOsrixTRaCPzNdA=
+github.com/gobuffalo/flect v0.0.0-20181007231023-ae7ed6bfe683/go.mod h1:rCiQgmAE4axgBNl3jZWzS5rETRYTGOsrixTRaCPzNdA=
+github.com/gobuffalo/flect v0.0.0-20181018182602-fd24a256709f/go.mod h1:rCiQgmAE4axgBNl3jZWzS5rETRYTGOsrixTRaCPzNdA=
+github.com/gobuffalo/flect v0.0.0-20181019110701-3d6f0b585514/go.mod h1:rCiQgmAE4axgBNl3jZWzS5rETRYTGOsrixTRaCPzNdA=
+github.com/gobuffalo/flect v0.0.0-20181024204909-8f6be1a8c6c2/go.mod h1:rCiQgmAE4axgBNl3jZWzS5rETRYTGOsrixTRaCPzNdA=
+github.com/gobuffalo/flect v0.0.0-20181104133451-1f6e9779237a/go.mod h1:rCiQgmAE4axgBNl3jZWzS5rETRYTGOsrixTRaCPzNdA=
+github.com/gobuffalo/flect v0.0.0-20181114183036-47375f6d8328/go.mod h1:0HvNbHdfh+WOvDSIASqJOSxTOWSxCCUF++k/Y53v9rI=
+github.com/gobuffalo/flect v0.0.0-20181210151238-24a2b68e0316/go.mod h1:en58vff74S9b99Eg42Dr+/9yPu437QjlNsO/hBYPuOk=
+github.com/gobuffalo/flect v0.0.0-20190104192022-4af577e09bf2/go.mod h1:en58vff74S9b99Eg42Dr+/9yPu437QjlNsO/hBYPuOk=
+github.com/gobuffalo/flect v0.0.0-20190117212819-a62e61d96794/go.mod h1:397QT6v05LkZkn07oJXXT6y9FCfwC8Pug0WA2/2mE9k=
+github.com/gobuffalo/flect v0.1.5/go.mod h1:W3K3X9ksuZfir8f/LrfVtWmCDQFfayuylOJ7sz/Fj80=
+github.com/gobuffalo/flect v0.2.0/go.mod h1:W3K3X9ksuZfir8f/LrfVtWmCDQFfayuylOJ7sz/Fj80=
+github.com/gobuffalo/flect v0.2.1/go.mod h1:vmkQwuZYhN5Pc4ljYQZzP+1sq+NEkK+lh20jmEmX3jc=
+github.com/gobuffalo/genny v0.0.0-20180924032338-7af3a40f2252/go.mod h1:tUTQOogrr7tAQnhajMSH6rv1BVev34H2sa1xNHMy94g=
+github.com/gobuffalo/genny v0.0.0-20181003150629-3786a0744c5d/go.mod h1:WAd8HmjMVrnkAZbmfgH5dLBUchsZfqzp/WS5sQz+uTM=
+github.com/gobuffalo/genny v0.0.0-20181005145118-318a41a134cc/go.mod h1:WAd8HmjMVrnkAZbmfgH5dLBUchsZfqzp/WS5sQz+uTM=
+github.com/gobuffalo/genny v0.0.0-20181007153042-b8de7d566757/go.mod h1:+oG5Ljrw04czAHbPXREwaFojJbpUvcIy4DiOnbEJFTA=
+github.com/gobuffalo/genny v0.0.0-20181012161047-33e5f43d83a6/go.mod h1:+oG5Ljrw04czAHbPXREwaFojJbpUvcIy4DiOnbEJFTA=
+github.com/gobuffalo/genny v0.0.0-20181017160347-90a774534246/go.mod h1:+oG5Ljrw04czAHbPXREwaFojJbpUvcIy4DiOnbEJFTA=
+github.com/gobuffalo/genny v0.0.0-20181024195656-51392254bf53/go.mod h1:o9GEH5gn5sCKLVB5rHFC4tq40rQ3VRUzmx6WwmaqISE=
+github.com/gobuffalo/genny v0.0.0-20181025145300-af3f81d526b8/go.mod h1:uZ1fFYvdcP8mu0B/Ynarf6dsGvp7QFIpk/QACUuFUVI=
+github.com/gobuffalo/genny v0.0.0-20181027191429-94d6cfb5c7fc/go.mod h1:x7SkrQQBx204Y+O9EwRXeszLJDTaWN0GnEasxgLrQTA=
+github.com/gobuffalo/genny v0.0.0-20181027195209-3887b7171c4f/go.mod h1:JbKx8HSWICu5zyqWOa0dVV1pbbXOHusrSzQUprW6g+w=
+github.com/gobuffalo/genny v0.0.0-20181106193839-7dcb0924caf1/go.mod h1:x61yHxvbDCgQ/7cOAbJCacZQuHgB0KMSzoYcw5debjU=
+github.com/gobuffalo/genny v0.0.0-20181107223128-f18346459dbe/go.mod h1:utQD3aKKEsdb03oR+Vi/6ztQb1j7pO10N3OBoowRcSU=
+github.com/gobuffalo/genny v0.0.0-20181114215459-0a4decd77f5d/go.mod h1:kN2KZ8VgXF9VIIOj/GM0Eo7YK+un4Q3tTreKOf0q1ng=
+github.com/gobuffalo/genny v0.0.0-20181119162812-e8ff4adce8bb/go.mod h1:BA9htSe4bZwBDJLe8CUkoqkypq3hn3+CkoHqVOW718E=
+github.com/gobuffalo/genny v0.0.0-20181127225641-2d959acc795b/go.mod h1:l54xLXNkteX/PdZ+HlgPk1qtcrgeOr3XUBBPDbH+7CQ=
+github.com/gobuffalo/genny v0.0.0-20181128191930-77e34f71ba2a/go.mod h1:FW/D9p7cEEOqxYA71/hnrkOWm62JZ5ZNxcNIVJEaWBU=
+github.com/gobuffalo/genny v0.0.0-20181203165245-fda8bcce96b1/go.mod h1:wpNSANu9UErftfiaAlz1pDZclrYzLtO5lALifODyjuM=
+github.com/gobuffalo/genny v0.0.0-20181203201232-849d2c9534ea/go.mod h1:wpNSANu9UErftfiaAlz1pDZclrYzLtO5lALifODyjuM=
+github.com/gobuffalo/genny v0.0.0-20181206121324-d6fb8a0dbe36/go.mod h1:wpNSANu9UErftfiaAlz1pDZclrYzLtO5lALifODyjuM=
+github.com/gobuffalo/genny v0.0.0-20181207164119-84844398a37d/go.mod h1:y0ysCHGGQf2T3vOhCrGHheYN54Y/REj0ayd0Suf4C/8=
+github.com/gobuffalo/genny v0.0.0-20181211165820-e26c8466f14d/go.mod h1:sHnK+ZSU4e2feXP3PA29ouij6PUEiN+RCwECjCTB3yM=
+github.com/gobuffalo/genny v0.0.0-20190104222617-a71664fc38e7/go.mod h1:QPsQ1FnhEsiU8f+O0qKWXz2RE4TiDqLVChWkBuh1WaY=
+github.com/gobuffalo/genny v0.0.0-20190112155932-f31a84fcacf5/go.mod h1:CIaHCrSIuJ4il6ka3Hub4DR4adDrGoXGEEt2FbBxoIo=
+github.com/gobuffalo/genny v0.2.0/go.mod h1:rWs4Z12d1Zbf19rlsn0nurr75KqhYp52EAGGxTbBhNk=
+github.com/gobuffalo/genny v0.3.0/go.mod h1:ywJ2CoXrTZj7rbS8HTbzv7uybnLKlsNSBhEQ+yFI3E8=
+github.com/gobuffalo/genny v0.6.0/go.mod h1:Vigx9VDiNscYpa/LwrURqGXLSIbzTfapt9+K6gF1kTA=
+github.com/gobuffalo/genny/v2 v2.0.5/go.mod h1:kRkJuAw9mdI37AiEYjV4Dl+TgkBDYf8HZVjLkqe5eBg=
+github.com/gobuffalo/gitgen v0.0.0-20190315122116-cc086187d211/go.mod h1:vEHJk/E9DmhejeLeNt7UVvlSGv3ziL+djtTr3yyzcOw=
+github.com/gobuffalo/github_flavored_markdown v1.0.4/go.mod h1:uRowCdK+q8d/RF0Kt3/DSalaIXbb0De/dmTqMQdkQ4I=
+github.com/gobuffalo/github_flavored_markdown v1.0.5/go.mod h1:U0643QShPF+OF2tJvYNiYDLDGDuQmJZXsf/bHOJPsMY=
+github.com/gobuffalo/github_flavored_markdown v1.0.7/go.mod h1:w93Pd9Lz6LvyQXEG6DktTPHkOtCbr+arAD5mkwMzXLI=
+github.com/gobuffalo/github_flavored_markdown v1.1.0/go.mod h1:TSpTKWcRTI0+v7W3x8dkSKMLJSUpuVitlptCkpeY8ic=
+github.com/gobuffalo/gogen v0.2.0/go.mod h1:V9QVDIxsgKNZs6L2IYiGR8datgMhB577vzTDqypH360=
+github.com/gobuffalo/helpers v0.2.2/go.mod h1:xYbzUdCUpVzLwLnqV8HIjT6hmG0Cs7YIBCJkNM597jw=
+github.com/gobuffalo/helpers v0.2.4/go.mod h1:NX7v27yxPDOPTgUFYmJ5ow37EbxdoLraucOGvMNawyk=
+github.com/gobuffalo/helpers v0.5.0/go.mod h1:stpgxJ2C7T99NLyAxGUnYMM2zAtBk5NKQR0SIbd05j4=
+github.com/gobuffalo/helpers v0.6.0/go.mod h1:pncVrer7x/KRvnL5aJABLAuT/RhKRR9klL6dkUOhyv8=
+github.com/gobuffalo/helpers v0.6.1/go.mod h1:wInbDi0vTJKZBviURTLRMFLE4+nF2uRuuL2fnlYo7w4=
+github.com/gobuffalo/here v0.6.0/go.mod h1:wAG085dHOYqUpf+Ap+WOdrPTp5IYcDAs/x7PLa8Y5fM=
+github.com/gobuffalo/httptest v1.0.2/go.mod h1:7T1IbSrg60ankme0aDLVnEY0h056g9M1/ZvpVThtB7E=
+github.com/gobuffalo/licenser v0.0.0-20180924033006-eae28e638a42/go.mod h1:Ubo90Np8gpsSZqNScZZkVXXAo5DGhTb+WYFIjlnog8w=
+github.com/gobuffalo/licenser v0.0.0-20181025145548-437d89de4f75/go.mod h1:x3lEpYxkRG/XtGCUNkio+6RZ/dlOvLzTI9M1auIwFcw=
+github.com/gobuffalo/licenser v0.0.0-20181027200154-58051a75da95/go.mod h1:BzhaaxGd1tq1+OLKObzgdCV9kqVhbTulxOpYbvMQWS0=
+github.com/gobuffalo/licenser v0.0.0-20181109171355-91a2a7aac9a7/go.mod h1:m+Ygox92pi9bdg+gVaycvqE8RVSjZp7mWw75+K5NPHk=
+github.com/gobuffalo/licenser v0.0.0-20181128165715-cc7305f8abed/go.mod h1:oU9F9UCE+AzI/MueCKZamsezGOOHfSirltllOVeRTAE=
+github.com/gobuffalo/licenser v0.0.0-20181203160806-fe900bbede07/go.mod h1:ph6VDNvOzt1CdfaWC+9XwcBnlSTBz2j49PBwum6RFaU=
+github.com/gobuffalo/licenser v0.0.0-20181211173111-f8a311c51159/go.mod h1:ve/Ue99DRuvnTaLq2zKa6F4KtHiYf7W046tDjuGYPfM=
+github.com/gobuffalo/licenser v1.1.0/go.mod h1:ZVWE6uKUE3rGf7sedUHWVjNWrEgxaUQLVFL+pQiWpfY=
+github.com/gobuffalo/logger v0.0.0-20181022175615-46cfb361fc27/go.mod h1:8sQkgyhWipz1mIctHF4jTxmJh1Vxhp7mP8IqbljgJZo=
+github.com/gobuffalo/logger v0.0.0-20181027144941-73d08d2bb969/go.mod h1:7uGg2duHKpWnN4+YmyKBdLXfhopkAdVM6H3nKbyFbz8=
+github.com/gobuffalo/logger v0.0.0-20181027193913-9cf4dd0efe46/go.mod h1:7uGg2duHKpWnN4+YmyKBdLXfhopkAdVM6H3nKbyFbz8=
+github.com/gobuffalo/logger v0.0.0-20181109185836-3feeab578c17/go.mod h1:oNErH0xLe+utO+OW8ptXMSA5DkiSEDW1u3zGIt8F9Ew=
+github.com/gobuffalo/logger v0.0.0-20181117211126-8e9b89b7c264/go.mod h1:5etB91IE0uBlw9k756fVKZJdS+7M7ejVhmpXXiSFj0I=
+github.com/gobuffalo/logger v0.0.0-20181127160119-5b956e21995c/go.mod h1:+HxKANrR9VGw9yN3aOAppJKvhO05ctDi63w4mDnKv2U=
+github.com/gobuffalo/logger v0.0.0-20190315122211-86e12af44bc2/go.mod h1:QdxcLw541hSGtBnhUc4gaNIXRjiDppFGaDqzbrBd3v8=
+github.com/gobuffalo/logger v1.0.0/go.mod h1:2zbswyIUa45I+c+FLXuWl9zSWEiVuthsk8ze5s8JvPs=
+github.com/gobuffalo/logger v1.0.1/go.mod h1:2zbswyIUa45I+c+FLXuWl9zSWEiVuthsk8ze5s8JvPs=
+github.com/gobuffalo/logger v1.0.3/go.mod h1:SoeejUwldiS7ZsyCBphOGURmWdwUFXs0J7TCjEhjKxM=
+github.com/gobuffalo/makr v1.1.5/go.mod h1:Y+o0btAH1kYAMDJW/TX3+oAXEu0bmSLLoC9mIFxtzOw=
+github.com/gobuffalo/mapi v1.0.0/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc=
+github.com/gobuffalo/mapi v1.0.1/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc=
+github.com/gobuffalo/mapi v1.0.2/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc=
+github.com/gobuffalo/mapi v1.1.0/go.mod h1:pqQ1XAqvpy/JYtRwoieNps2yU8MFiMxBUpAm2FBtQ50=
+github.com/gobuffalo/mapi v1.2.1/go.mod h1:giGJ2AUESRepOFYAzWpq8Gf/s/QDryxoEHisQtFN3cY=
+github.com/gobuffalo/meta v0.0.0-20181018155829-df62557efcd3/go.mod h1:XTTOhwMNryif3x9LkTTBO/Llrveezd71u3quLd0u7CM=
+github.com/gobuffalo/meta v0.0.0-20181018192820-8c6cef77dab3/go.mod h1:E94EPzx9NERGCY69UWlcj6Hipf2uK/vnfrF4QD0plVE=
+github.com/gobuffalo/meta v0.0.0-20181025145500-3a985a084b0a/go.mod h1:YDAKBud2FP7NZdruCSlmTmDOZbVSa6bpK7LJ/A/nlKg=
+github.com/gobuffalo/meta v0.0.0-20181114191255-b130ebedd2f7/go.mod h1:K6cRZ29ozr4Btvsqkjvg5nDFTLOgTqf03KA70Ks0ypE=
+github.com/gobuffalo/meta v0.0.0-20181127070345-0d7e59dd540b/go.mod h1:RLO7tMvE0IAKAM8wny1aN12pvEKn7EtkBLkUZR00Qf8=
+github.com/gobuffalo/meta v0.0.0-20190120163247-50bbb1fa260d/go.mod h1:KKsH44nIK2gA8p0PJmRT9GvWJUdphkDUA8AJEvFWiqM=
+github.com/gobuffalo/meta v0.0.0-20190329152330-e161e8a93e3b/go.mod h1:mCRSy5F47tjK8yaIDcJad4oe9fXxY5gLrx3Xx2spK+0=
+github.com/gobuffalo/meta v0.3.0/go.mod h1:cpr6mrUX5H/B4wEP86Gdq568TK4+dKUD8oRPl698RUw=
+github.com/gobuffalo/mw-basicauth v1.0.3/go.mod h1:dg7+ilMZOKnQFHDefUzUHufNyTswVUviCBgF244C1+0=
+github.com/gobuffalo/mw-contenttype v0.0.0-20180802152300-74f5a47f4d56/go.mod h1:7EvcmzBbeCvFtQm5GqF9ys6QnCxz2UM1x0moiWLq1No=
+github.com/gobuffalo/mw-csrf v0.0.0-20180802151833-446ff26e108b/go.mod h1:sbGtb8DmDZuDUQoxjr8hG1ZbLtZboD9xsn6p77ppcHo=
+github.com/gobuffalo/mw-forcessl v0.0.0-20180802152810-73921ae7a130/go.mod h1:JvNHRj7bYNAMUr/5XMkZaDcw3jZhUZpsmzhd//FFWmQ=
+github.com/gobuffalo/mw-i18n v0.0.0-20180802152014-e3060b7e13d6/go.mod h1:91AQfukc52A6hdfIfkxzyr+kpVYDodgAeT5cjX1UIj4=
+github.com/gobuffalo/mw-paramlogger v0.0.0-20181005191442-d6ee392ec72e/go.mod h1:6OJr6VwSzgJMqWMj7TYmRUqzNe2LXu/W1rRW4MAz/ME=
+github.com/gobuffalo/mw-tokenauth v0.0.0-20181001105134-8545f626c189/go.mod h1:UqBF00IfKvd39ni5+yI5MLMjAf4gX7cDKN/26zDOD6c=
+github.com/gobuffalo/nulls v0.2.0/go.mod h1:w4q8RoSCEt87Q0K0sRIZWYeIxkxog5mh3eN3C/n+dUc=
+github.com/gobuffalo/nulls v0.3.0/go.mod h1:UP49vd/k+bcaz6m0cHMyuk8oQ7XgLnkfxeiVoPAvBSs=
+github.com/gobuffalo/packd v0.0.0-20181027182251-01ad393492c8/go.mod h1:SmdBdhj6uhOsg1Ui4SFAyrhuc7U4VCildosO5IDJ3lc=
+github.com/gobuffalo/packd v0.0.0-20181027190505-aafc0d02c411/go.mod h1:SmdBdhj6uhOsg1Ui4SFAyrhuc7U4VCildosO5IDJ3lc=
+github.com/gobuffalo/packd v0.0.0-20181027194105-7ae579e6d213/go.mod h1:SmdBdhj6uhOsg1Ui4SFAyrhuc7U4VCildosO5IDJ3lc=
+github.com/gobuffalo/packd v0.0.0-20181031195726-c82734870264/go.mod h1:Yf2toFaISlyQrr5TfO3h6DB9pl9mZRmyvBGQb/aQ/pI=
+github.com/gobuffalo/packd v0.0.0-20181104210303-d376b15f8e96/go.mod h1:Yf2toFaISlyQrr5TfO3h6DB9pl9mZRmyvBGQb/aQ/pI=
+github.com/gobuffalo/packd v0.0.0-20181111195323-b2e760a5f0ff/go.mod h1:Yf2toFaISlyQrr5TfO3h6DB9pl9mZRmyvBGQb/aQ/pI=
+github.com/gobuffalo/packd v0.0.0-20181114190715-f25c5d2471d7/go.mod h1:Yf2toFaISlyQrr5TfO3h6DB9pl9mZRmyvBGQb/aQ/pI=
+github.com/gobuffalo/packd v0.0.0-20181124090624-311c6248e5fb/go.mod h1:Foenia9ZvITEvG05ab6XpiD5EfBHPL8A6hush8SJ0o8=
+github.com/gobuffalo/packd v0.0.0-20181207120301-c49825f8f6f4/go.mod h1:LYc0TGKFBBFTRC9dg2pcRcMqGCTMD7T2BIMP7OBuQAA=
+github.com/gobuffalo/packd v0.0.0-20181212173646-eca3b8fd6687/go.mod h1:LYc0TGKFBBFTRC9dg2pcRcMqGCTMD7T2BIMP7OBuQAA=
+github.com/gobuffalo/packd v0.1.0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4=
+github.com/gobuffalo/packd v0.2.0/go.mod h1:k2CkHP3bjbqL2GwxwhxUy1DgnlbW644hkLC9iIUvZwY=
+github.com/gobuffalo/packd v0.3.0/go.mod h1:zC7QkmNkYVGKPw4tHpBQ+ml7W/3tIebgeo1b36chA3Q=
+github.com/gobuffalo/packd v1.0.0/go.mod h1:6VTc4htmJRFB7u1m/4LeMTWjFoYrUiBkU9Fdec9hrhI=
+github.com/gobuffalo/packr v1.13.7/go.mod h1:KkinLIn/n6+3tVXMwg6KkNvWwVsrRAz4ph+jgpk3Z24=
+github.com/gobuffalo/packr v1.15.0/go.mod h1:t5gXzEhIviQwVlNx/+3SfS07GS+cZ2hn76WLzPp6MGI=
+github.com/gobuffalo/packr v1.15.1/go.mod h1:IeqicJ7jm8182yrVmNbM6PR4g79SjN9tZLH8KduZZwE=
+github.com/gobuffalo/packr v1.19.0/go.mod h1:MstrNkfCQhd5o+Ct4IJ0skWlxN8emOq8DsoT1G98VIU=
+github.com/gobuffalo/packr v1.20.0/go.mod h1:JDytk1t2gP+my1ig7iI4NcVaXr886+N0ecUga6884zw=
+github.com/gobuffalo/packr v1.21.0/go.mod h1:H00jGfj1qFKxscFJSw8wcL4hpQtPe1PfU2wa6sg/SR0=
+github.com/gobuffalo/packr v1.22.0/go.mod h1:Qr3Wtxr3+HuQEwWqlLnNW4t1oTvK+7Gc/Rnoi/lDFvA=
+github.com/gobuffalo/packr/v2 v2.0.0-rc.8/go.mod h1:y60QCdzwuMwO2R49fdQhsjCPv7tLQFR0ayzxxla9zes=
+github.com/gobuffalo/packr/v2 v2.0.0-rc.9/go.mod h1:fQqADRfZpEsgkc7c/K7aMew3n4aF1Kji7+lIZeR98Fc=
+github.com/gobuffalo/packr/v2 v2.0.0-rc.10/go.mod h1:4CWWn4I5T3v4c1OsJ55HbHlUEKNWMITG5iIkdr4Px4w=
+github.com/gobuffalo/packr/v2 v2.0.0-rc.11/go.mod h1:JoieH/3h3U4UmatmV93QmqyPUdf4wVM9HELaHEu+3fk=
+github.com/gobuffalo/packr/v2 v2.0.0-rc.12/go.mod h1:FV1zZTsVFi1DSCboO36Xgs4pzCZBjB/tDV9Cz/lSaR8=
+github.com/gobuffalo/packr/v2 v2.0.0-rc.13/go.mod h1:2Mp7GhBFMdJlOK8vGfl7SYtfMP3+5roE39ejlfjw0rA=
+github.com/gobuffalo/packr/v2 v2.0.0-rc.14/go.mod h1:06otbrNvDKO1eNQ3b8hst+1010UooI2MFg+B2Ze4MV8=
+github.com/gobuffalo/packr/v2 v2.0.0-rc.15/go.mod h1:IMe7H2nJvcKXSF90y4X1rjYIRlNMJYCxEhssBXNZwWs=
+github.com/gobuffalo/packr/v2 v2.4.0/go.mod h1:ra341gygw9/61nSjAbfwcwh8IrYL4WmR4IsPkPBhQiY=
+github.com/gobuffalo/packr/v2 v2.5.2/go.mod h1:sgEE1xNZ6G0FNN5xn9pevVu4nywaxHvgup67xisti08=
+github.com/gobuffalo/packr/v2 v2.7.1/go.mod h1:qYEvAazPaVxy7Y7KR0W8qYEE+RymX74kETFqjFoFlOc=
+github.com/gobuffalo/plush v3.7.16+incompatible/go.mod h1:rQ4zdtUUyZNqULlc6bqd5scsPfLKfT0+TGMChgduDvI=
+github.com/gobuffalo/plush v3.7.20+incompatible/go.mod h1:rQ4zdtUUyZNqULlc6bqd5scsPfLKfT0+TGMChgduDvI=
+github.com/gobuffalo/plush v3.7.21+incompatible/go.mod h1:rQ4zdtUUyZNqULlc6bqd5scsPfLKfT0+TGMChgduDvI=
+github.com/gobuffalo/plush v3.7.22+incompatible/go.mod h1:rQ4zdtUUyZNqULlc6bqd5scsPfLKfT0+TGMChgduDvI=
+github.com/gobuffalo/plush v3.7.23+incompatible/go.mod h1:rQ4zdtUUyZNqULlc6bqd5scsPfLKfT0+TGMChgduDvI=
+github.com/gobuffalo/plush v3.7.30+incompatible/go.mod h1:rQ4zdtUUyZNqULlc6bqd5scsPfLKfT0+TGMChgduDvI=
+github.com/gobuffalo/plush v3.7.31+incompatible/go.mod h1:rQ4zdtUUyZNqULlc6bqd5scsPfLKfT0+TGMChgduDvI=
+github.com/gobuffalo/plush v3.7.32+incompatible/go.mod h1:rQ4zdtUUyZNqULlc6bqd5scsPfLKfT0+TGMChgduDvI=
+github.com/gobuffalo/plush v3.8.2+incompatible/go.mod h1:rQ4zdtUUyZNqULlc6bqd5scsPfLKfT0+TGMChgduDvI=
+github.com/gobuffalo/plush v3.8.3+incompatible/go.mod h1:rQ4zdtUUyZNqULlc6bqd5scsPfLKfT0+TGMChgduDvI=
+github.com/gobuffalo/plush/v4 v4.0.0/go.mod h1:ErFS3UxKqEb8fpFJT7lYErfN/Nw6vHGiDMTjxpk5bQ0=
+github.com/gobuffalo/plushgen v0.0.0-20181128164830-d29dcb966cb2/go.mod h1:r9QwptTFnuvSaSRjpSp4S2/4e2D3tJhARYbvEBcKSb4=
+github.com/gobuffalo/plushgen v0.0.0-20181203163832-9fc4964505c2/go.mod h1:opEdT33AA2HdrIwK1aibqnTJDVVKXC02Bar/GT1YRVs=
+github.com/gobuffalo/plushgen v0.0.0-20181207152837-eedb135bd51b/go.mod h1:Lcw7HQbEVm09sAQrCLzIxuhFbB3nAgp4c55E+UlynR0=
+github.com/gobuffalo/plushgen v0.0.0-20190104222512-177cd2b872b3/go.mod h1:tYxCozi8X62bpZyKXYHw1ncx2ZtT2nFvG42kuLwYjoc=
+github.com/gobuffalo/plushgen v0.1.2/go.mod h1:3U71v6HWZpVER1nInTXeAwdoRNsRd4W8aeIa1Lyp+Bk=
+github.com/gobuffalo/pop v4.8.2+incompatible/go.mod h1:DwBz3SD5SsHpTZiTubcsFWcVDpJWGsxjVjMPnkiThWg=
+github.com/gobuffalo/pop v4.8.3+incompatible/go.mod h1:DwBz3SD5SsHpTZiTubcsFWcVDpJWGsxjVjMPnkiThWg=
+github.com/gobuffalo/pop v4.8.4+incompatible/go.mod h1:DwBz3SD5SsHpTZiTubcsFWcVDpJWGsxjVjMPnkiThWg=
+github.com/gobuffalo/pop v4.13.1+incompatible/go.mod h1:DwBz3SD5SsHpTZiTubcsFWcVDpJWGsxjVjMPnkiThWg=
+github.com/gobuffalo/pop/v5 v5.0.11/go.mod h1:mZJHJbA3cy2V18abXYuVop2ldEJ8UZ2DK6qOekC5u5g=
+github.com/gobuffalo/release v1.0.35/go.mod h1:VtHFAKs61vO3wboCec5xr9JPTjYyWYcvaM3lclkc4x4=
+github.com/gobuffalo/release v1.0.38/go.mod h1:VtHFAKs61vO3wboCec5xr9JPTjYyWYcvaM3lclkc4x4=
+github.com/gobuffalo/release v1.0.42/go.mod h1:RPs7EtafH4oylgetOJpGP0yCZZUiO4vqHfTHJjSdpug=
+github.com/gobuffalo/release v1.0.52/go.mod h1:RPs7EtafH4oylgetOJpGP0yCZZUiO4vqHfTHJjSdpug=
+github.com/gobuffalo/release v1.0.53/go.mod h1:FdF257nd8rqhNaqtDWFGhxdJ/Ig4J7VcS3KL7n/a+aA=
+github.com/gobuffalo/release v1.0.54/go.mod h1:Pe5/RxRa/BE8whDpGfRqSI7D1a0evGK1T4JDm339tJc=
+github.com/gobuffalo/release v1.0.61/go.mod h1:mfIO38ujUNVDlBziIYqXquYfBF+8FDHUjKZgYC1Hj24=
+github.com/gobuffalo/release v1.0.72/go.mod h1:NP5NXgg/IX3M5XmHmWR99D687/3Dt9qZtTK/Lbwc1hU=
+github.com/gobuffalo/release v1.1.1/go.mod h1:Sluak1Xd6kcp6snkluR1jeXAogdJZpFFRzTYRs/2uwg=
+github.com/gobuffalo/release v1.1.3/go.mod h1:CuXc5/m+4zuq8idoDt1l4va0AXAn/OSs08uHOfMVr8E=
+github.com/gobuffalo/release v1.1.6/go.mod h1:18naWa3kBsqO0cItXZNJuefCKOENpbbUIqRL1g+p6z0=
+github.com/gobuffalo/release v1.7.0/go.mod h1:xH2NjAueVSY89XgC4qx24ojEQ4zQ9XCGVs5eXwJTkEs=
+github.com/gobuffalo/shoulders v1.0.1/go.mod h1:V33CcVmaQ4gRUmHKwq1fiTXuf8Gp/qjQBUL5tHPmvbA=
+github.com/gobuffalo/shoulders v1.0.4/go.mod h1:LqMcHhKRuBPMAYElqOe3POHiZ1x7Ry0BE8ZZ84Bx+k4=
+github.com/gobuffalo/syncx v0.0.0-20181120191700-98333ab04150/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw=
+github.com/gobuffalo/syncx v0.0.0-20181120194010-558ac7de985f/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw=
+github.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw=
+github.com/gobuffalo/syncx v0.1.0/go.mod h1:Mg/s+5pv7IgxEp6sA+NFpqS4o2x+R9dQNwbwT0iuOGQ=
+github.com/gobuffalo/tags v2.0.11+incompatible/go.mod h1:9XmhOkyaB7UzvuY4UoZO4s67q8/xRMVJEaakauVQYeY=
+github.com/gobuffalo/tags v2.0.14+incompatible/go.mod h1:9XmhOkyaB7UzvuY4UoZO4s67q8/xRMVJEaakauVQYeY=
+github.com/gobuffalo/tags v2.0.15+incompatible/go.mod h1:9XmhOkyaB7UzvuY4UoZO4s67q8/xRMVJEaakauVQYeY=
+github.com/gobuffalo/tags v2.1.0+incompatible/go.mod h1:9XmhOkyaB7UzvuY4UoZO4s67q8/xRMVJEaakauVQYeY=
+github.com/gobuffalo/tags v2.1.7+incompatible/go.mod h1:9XmhOkyaB7UzvuY4UoZO4s67q8/xRMVJEaakauVQYeY=
+github.com/gobuffalo/tags/v3 v3.0.2/go.mod h1:ZQeN6TCTiwAFnS0dNcbDtSgZDwNKSpqajvVtt6mlYpA=
+github.com/gobuffalo/tags/v3 v3.1.0/go.mod h1:ZQeN6TCTiwAFnS0dNcbDtSgZDwNKSpqajvVtt6mlYpA=
+github.com/gobuffalo/uuid v2.0.3+incompatible/go.mod h1:ErhIzkRhm0FtRuiE/PeORqcw4cVi1RtSpnwYrxuvkfE=
+github.com/gobuffalo/uuid v2.0.4+incompatible/go.mod h1:ErhIzkRhm0FtRuiE/PeORqcw4cVi1RtSpnwYrxuvkfE=
+github.com/gobuffalo/uuid v2.0.5+incompatible/go.mod h1:ErhIzkRhm0FtRuiE/PeORqcw4cVi1RtSpnwYrxuvkfE=
+github.com/gobuffalo/validate v2.0.3+incompatible/go.mod h1:N+EtDe0J8252BgfzQUChBgfd6L93m9weay53EWFVsMM=
+github.com/gobuffalo/validate v2.0.4+incompatible/go.mod h1:N+EtDe0J8252BgfzQUChBgfd6L93m9weay53EWFVsMM=
+github.com/gobuffalo/validate/v3 v3.0.0/go.mod h1:HFpjq+AIiA2RHoQnQVTFKF/ZpUPXwyw82LgyDPxQ9r0=
+github.com/gobuffalo/validate/v3 v3.1.0/go.mod h1:HFpjq+AIiA2RHoQnQVTFKF/ZpUPXwyw82LgyDPxQ9r0=
+github.com/gobuffalo/validate/v3 v3.2.0/go.mod h1:PrhDOdDHxtN8KUgMvF3TDL0r1YZXV4sQnyFX/EmeETY=
+github.com/gobuffalo/x v0.0.0-20181003152136-452098b06085/go.mod h1:WevpGD+5YOreDJznWevcn8NTmQEW5STSBgIkpkjzqXc=
+github.com/gobuffalo/x v0.0.0-20181007152206-913e47c59ca7/go.mod h1:9rDPXaB3kXdKWzMc4odGQQdG2e2DIEmANy5aSJ9yesY=
+github.com/gofrs/uuid v3.1.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE=
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
+github.com/gofrs/uuid/v3 v3.1.2/go.mod h1:xPwMqoocQ1L5G6pXX5BcE7N5jlzn2o19oqAKxwZW/kI=
github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
+github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
+github.com/golang/gddo v0.0.0-20180828051604-96d2a289f41e/go.mod h1:xEhNfoBDX1hzLm2Nf80qUvZ2sVwoMZ8d6IE2SrsQfh4=
+github.com/golang/gddo v0.0.0-20190904175337-72a348e765d2/go.mod h1:xEhNfoBDX1hzLm2Nf80qUvZ2sVwoMZ8d6IE2SrsQfh4=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
@@ -152,8 +443,10 @@ github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4er
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/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
+github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/mock v1.5.0 h1:jlYHihg//f7RRwuPfptm04yp4s7O6Kw8EZiVYIGcH0g=
github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
+github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@@ -162,6 +455,7 @@ github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:x
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
+github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
@@ -173,6 +467,7 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
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-github/v27 v27.0.4/go.mod h1:/0Gr8pJ55COkmv+S/yPKCczSkUPIM/LnFyubufRNIS0=
+github.com/google/go-jsonnet v0.16.0/go.mod h1:sOcuej3UW1vpPTZOr8L7RQimqai1a57bt5j22LzGZCw=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
@@ -180,15 +475,28 @@ github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OI
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.1.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
+github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
+github.com/gopherjs/gopherjs v0.0.0-20181004151105-1babbf986f6f/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
+github.com/gorilla/mux v1.7.0/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
+github.com/gorilla/pat v0.0.0-20180118222023-199c85a7f6d1/go.mod h1:YeAe0gNeiNT5hoiZRI4yiOky6jVdNvfO2N6Kav/HmxY=
+github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
+github.com/gorilla/sessions v1.1.2/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
+github.com/gorilla/sessions v1.1.3/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
+github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
+github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
+github.com/gotestyourself/gotestyourself v1.3.0/go.mod h1:zZKM6oeNM8k+FRljX1mnzVYeS8wiGgQyvST1/GafPbY=
+github.com/gotestyourself/gotestyourself v2.2.0+incompatible/go.mod h1:zZKM6oeNM8k+FRljX1mnzVYeS8wiGgQyvST1/GafPbY=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
@@ -228,9 +536,11 @@ github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9
github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8=
github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
+github.com/jackc/fake v0.0.0-20150926172116-812a484cc733/go.mod h1:WrMFNQdiFJ80sQsxDoMokWK1W5TQtxBFNpzWTD84ibQ=
github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA=
github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE=
github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s=
+github.com/jackc/pgconn v1.3.2/go.mod h1:LvCquS3HbBKwgl7KbX9KyqEIumJAbm1UMcTvGaIf3bM=
github.com/jackc/pgconn v1.4.0/go.mod h1:Y2O3ZDF0q4mMacyWV3AstPJpeHXWGEetiFttmq5lahk=
github.com/jackc/pgconn v1.5.0/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI=
github.com/jackc/pgconn v1.5.1-0.20200601181101-fa742c524853/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI=
@@ -262,9 +572,13 @@ github.com/jackc/pgtype v1.3.1-0.20200510190516-8cd94a14c75a/go.mod h1:vaogEUkAL
github.com/jackc/pgtype v1.3.1-0.20200606141011-f6355165a91c/go.mod h1:cvk9Bgu/VzJ9/lxTO5R5sf80p0DiucVtN7ZxvaC4GmQ=
github.com/jackc/pgtype v1.7.0 h1:6f4kVsW01QftE38ufBYxKciO6gyioXSC0ABIRLcZrGs=
github.com/jackc/pgtype v1.7.0/go.mod h1:ZnHF+rMePVqDKaOfJVI4Q8IVvAQMryDlDkZnKOI75BE=
+github.com/jackc/pgx v3.2.0+incompatible/go.mod h1:0ZGrqGqkRlliWnWB4zKnWtjbSWbGkVEFm4TeybAXq+I=
+github.com/jackc/pgx v3.6.2+incompatible h1:2zP5OD7kiyR3xzRYMhOcXVvkDZsImVXfj+yIyTQf3/o=
+github.com/jackc/pgx v3.6.2+incompatible/go.mod h1:0ZGrqGqkRlliWnWB4zKnWtjbSWbGkVEFm4TeybAXq+I=
github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y=
github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM=
github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=
+github.com/jackc/pgx/v4 v4.4.1/go.mod h1:6iSW+JznC0YT+SgBn7rNxoEBsBgSmnC5FwyCekOGUiE=
github.com/jackc/pgx/v4 v4.5.0/go.mod h1:EpAKPLdnTorwmPUUsqrPxy5fphV18j9q3wrfRXgo+kA=
github.com/jackc/pgx/v4 v4.6.1-0.20200510190926-94ba730bb1e9/go.mod h1:t3/cdRQl6fOLDxqtlyhe9UWgfIi9R8+8v8GKV5TRA/o=
github.com/jackc/pgx/v4 v4.6.1-0.20200606145419-4e5062306904/go.mod h1:ZDaNWkt9sW1JMiNn0kdYBaLelIhw7Pg4qd+Vk6tw7Hg=
@@ -275,28 +589,50 @@ github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0f
github.com/jackc/puddle v1.1.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.1.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
+github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
+github.com/jmoiron/sqlx v0.0.0-20180614180643-0dae4fefe7c0/go.mod h1:IiEW3SEiiErVyFdH8NTuWjSifiEQKUoyK3LNqr2kCHU=
+github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks=
+github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901/go.mod h1:Z86h9688Y0wesXCyonoVr47MasHilkuLMqGhRZ4Hpak=
+github.com/joho/godotenv v1.2.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
+github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
+github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
+github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
+github.com/karrick/godirwalk v1.7.5/go.mod h1:2c9FRhkDxdIbgkOnCEvnSWs71Bhugbl46shStcFDJ34=
+github.com/karrick/godirwalk v1.7.7/go.mod h1:2c9FRhkDxdIbgkOnCEvnSWs71Bhugbl46shStcFDJ34=
+github.com/karrick/godirwalk v1.7.8/go.mod h1:2c9FRhkDxdIbgkOnCEvnSWs71Bhugbl46shStcFDJ34=
+github.com/karrick/godirwalk v1.10.3/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA=
+github.com/karrick/godirwalk v1.10.9/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA=
+github.com/karrick/godirwalk v1.10.12/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA=
+github.com/karrick/godirwalk v1.15.5/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk=
+github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.11.8 h1:difgzQsp5mdAz9v8lm3P/I+EpDKMU/6uTMw1y1FObuo=
github.com/klauspost/compress v1.11.8/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
+github.com/konsorten/go-windows-terminal-sequences v0.0.0-20180402223658-b729f2633dfe/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
-github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs=
+github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/lib/pq v0.0.0-20180327071824-d34b9ff171c2/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
@@ -305,12 +641,37 @@ github.com/lib/pq v1.10.1 h1:6VXZrLU0jHBYyAqrSPa+MgPfnSvTPuMgK+k0o5kVFWo=
github.com/lib/pq v1.10.1/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM=
github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4=
+github.com/luna-duclos/instrumentedsql v0.0.0-20181127104832-b7d587d28109/go.mod h1:PWUIzhtavmOR965zfawVsHXbEuU1G29BPZ/CB3C7jXk=
+github.com/luna-duclos/instrumentedsql v1.1.2/go.mod h1:4LGbEqDnopzNAiyxPPDXhLspyunZxgPTMJBKtC6U0BQ=
github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ=
+github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
+github.com/markbates/deplist v1.0.4/go.mod h1:gRRbPbbuA8TmMiRvaOzUlRfzfjeCCBqX2A6arxN01MM=
+github.com/markbates/deplist v1.0.5/go.mod h1:gRRbPbbuA8TmMiRvaOzUlRfzfjeCCBqX2A6arxN01MM=
+github.com/markbates/deplist v1.1.3/go.mod h1:BF7ioVzAJYEtzQN/os4rt8H8Ti3h0T7EoN+7eyALktE=
+github.com/markbates/going v1.0.2/go.mod h1:UWCk3zm0UKefHZ7l8BNqi26UyiEMniznk8naLdTcy6c=
+github.com/markbates/grift v1.0.4/go.mod h1:wbmtW74veyx+cgfwFhlnnMWqhoz55rnHR47oMXzsyVs=
+github.com/markbates/hmax v1.0.0/go.mod h1:cOkR9dktiESxIMu+65oc/r/bdY4bE8zZw3OLhLx0X2c=
+github.com/markbates/inflect v1.0.0/go.mod h1:oTeZL2KHA7CUX6X+fovmK9OvIOFuqu0TwdQrZjLTh88=
+github.com/markbates/inflect v1.0.1/go.mod h1:uv3UVNBe5qBIfCm8O8Q+DW+S1EopeyINj+Ikhc7rnCk=
+github.com/markbates/inflect v1.0.3/go.mod h1:1fR9+pO2KHEO9ZRtto13gDwwZaAKstQzferVeWqbgNs=
+github.com/markbates/inflect v1.0.4/go.mod h1:1fR9+pO2KHEO9ZRtto13gDwwZaAKstQzferVeWqbgNs=
+github.com/markbates/oncer v0.0.0-20180924031910-e862a676800b/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE=
+github.com/markbates/oncer v0.0.0-20180924034138-723ad0170a46/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE=
+github.com/markbates/oncer v0.0.0-20181014194634-05fccaae8fc4/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE=
+github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE=
+github.com/markbates/oncer v1.0.0/go.mod h1:Z59JA581E9GP6w96jai+TGqafHPW+cPfRxz2aSZ0mcI=
+github.com/markbates/pkger v0.17.1/go.mod h1:0JoVlrol20BSywW79rN3kdFFsE5xYM+rSCQDXbLhiuI=
+github.com/markbates/refresh v1.4.10/go.mod h1:NDPHvotuZmTmesXxr95C9bjlw1/0frJwtME2dzcVKhc=
+github.com/markbates/safe v1.0.0/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0=
+github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0=
+github.com/markbates/sigtx v1.0.0/go.mod h1:QF1Hv6Ic6Ca6W+T+DL0Y/ypborFKyvUY9HmuCD4VeTc=
+github.com/markbates/willie v1.0.9/go.mod h1:fsrFVWl91+gXpx/6dv715j7i11fYPfZ9ZGfH0DQzY7w=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
+github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
@@ -318,11 +679,21 @@ github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
+github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
-github.com/mattn/go-sqlite3 v1.14.7 h1:fxWBnXkxfM6sRiuH3bqJ4CfzZojMOLVc0UTsTglEghA=
+github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
+github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
+github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-sqlite3 v1.14.7/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
+github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
+github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
+github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw=
+github.com/mattn/goveralls v0.0.6 h1:cr8Y0VMo/MnEZBjxNN/vh6G90SZ7IMb6lms1dzMoO+Y=
+github.com/mattn/goveralls v0.0.6/go.mod h1:h8b4ow6FxSPMQHF6o2ve3qsclnffZjYTNEKmLesRwqw=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
+github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
+github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
@@ -331,12 +702,19 @@ github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eI
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
-github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
+github.com/mitchellh/mapstructure v1.0.0/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
+github.com/mitchellh/mapstructure v1.2.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
+github.com/mitchellh/mapstructure v1.3.2 h1:mRS76wmkOn3KkKAyXDu42V+6ebnXWIztFSYGN7GeoRg=
+github.com/mitchellh/mapstructure v1.3.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+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/monoculum/formam v0.0.0-20180901015400-4e68be1d79ba/go.mod h1:RKgILGEJq24YyJ2ban8EO0RUVSJlF1pGsEvoLEACr/Q=
+github.com/moul/http2curl v0.0.0-20170919181001-9ac6cf4d929b/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg=
github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU=
@@ -345,31 +723,74 @@ github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzE
github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
+github.com/nicksnyder/go-i18n v1.10.0/go.mod h1:HrK7VCrbOvQoUAQ7Vpy7i87N7JZZZ7R2xBGjv0j365Q=
github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs=
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
+github.com/oleiade/reflections v1.0.0 h1:0ir4pc6v8/PJ0yw5AEtMddfXpWBXg9cnG7SgSoJuCgY=
+github.com/oleiade/reflections v1.0.0/go.mod h1:RbATFBbKYkVdqmSFtx13Bb/tVhR0lgOBXunWTZKeL4w=
github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.9.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.14.2 h1:8mVmC9kjFFmA8H4pKMUhcblgifdkOIXPvbhN1T36q1M=
github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
+github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
+github.com/onsi/gomega v1.4.2/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
+github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
+github.com/onsi/gomega v1.6.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
+github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.10.3 h1:gph6h/qe9GSUw1NhH1gp+qb+h8rXD8Cy60Z32Qw3ELA=
github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc=
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
+github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
+github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
+github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
+github.com/opencontainers/runc v1.0.0-rc9/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis=
github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74=
github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
+github.com/opentracing/opentracing-go v1.1.1-0.20190913142402-a7454ce5950e/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA=
github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
+github.com/ory/analytics-go/v4 v4.0.0/go.mod h1:FMx9cLRD9xN+XevPvZ5FDMfignpmcqPP6FUKnJ9/MmE=
+github.com/ory/dockertest v3.3.5+incompatible/go.mod h1:1vX4m9wsvi00u5bseYwXaSnhNrne+V0E6LAcBILJdPs=
+github.com/ory/dockertest/v3 v3.5.4/go.mod h1:J8ZUbNB2FOhm1cFZW9xBpDsODqsSWcyYgtJYVPcnF70=
+github.com/ory/fosite v0.29.0/go.mod h1:0atSZmXO7CAcs6NPMI/Qtot8tmZYj04Nddoold4S2h0=
+github.com/ory/fosite v0.39.0 h1:u1Ct/ME7XYzREvufr7ehBIdq/KatjVLIYg/ABqWzprw=
+github.com/ory/fosite v0.39.0/go.mod h1:37r59qkOSPueYKmaA7EHiXrDMF1B+XPN+MgkZgTRg3Y=
+github.com/ory/go-acc v0.0.0-20181118080137-ddc355013f90/go.mod h1:sxnvPCxChFuSmTJGj8FdMupeq1BezCiEpDjTUXQ4hf4=
+github.com/ory/go-acc v0.2.5 h1:31irXHzG2vnKQSE4weJm7AdfrnpaVjVCq3nD7viXCJE=
+github.com/ory/go-acc v0.2.5/go.mod h1:4Kb/UnPcT8qRAk3IAxta+hvVapdxTLWtrr7bFLlEgpw=
+github.com/ory/go-convenience v0.1.0 h1:zouLKfF2GoSGnJwGq+PE/nJAE6dj2Zj5QlTgmMTsTS8=
+github.com/ory/go-convenience v0.1.0/go.mod h1:uEY/a60PL5c12nYz4V5cHY03IBmwIAEm8TWB0yn9KNs=
+github.com/ory/gojsonreference v0.0.0-20190720135523-6b606c2d8ee8/go.mod h1:wsH1C4nIeeQClDtD5AH7kF1uTS6zWyqfjVDTmB0Em7A=
+github.com/ory/gojsonschema v1.1.1-0.20190919112458-f254ca73d5e9/go.mod h1:BNZpdJgB74KOLSsWFvzw6roXg1I6O51WO8roMmW+T7Y=
+github.com/ory/herodot v0.6.2/go.mod h1:3BOneqcyBsVybCPAJoi92KN2BpJHcmDqAMcAAaJiJow=
+github.com/ory/herodot v0.7.0/go.mod h1:YXKOfAXYdQojDP5sD8m0ajowq3+QXNdtxA+QiUXBwn0=
+github.com/ory/herodot v0.8.3/go.mod h1:rvLjxOAlU5omtmgjCfazQX2N82EpMfl3BytBWc1jjsk=
+github.com/ory/jsonschema/v3 v3.0.1/go.mod h1:jgLHekkFk0uiGdEWGleC+tOm6JSSP8cbf17PnBuGXlw=
+github.com/ory/viper v1.5.6/go.mod h1:TYmpFpKLxjQwvT4f0QPpkOn4sDXU1kDgAwJpgLYiQ28=
+github.com/ory/viper v1.7.4/go.mod h1:T6sodNZKNGPpashUOk7EtXz2isovz8oCd57GNVkkNmE=
+github.com/ory/viper v1.7.5 h1:+xVdq7SU3e1vNaCsk/ixsfxE4zylk1TJUiJrY647jUE=
+github.com/ory/viper v1.7.5/go.mod h1:ypOuyJmEUb3oENywQZRgeAMwqgOyDqwboO1tj3DjTaM=
+github.com/ory/x v0.0.84/go.mod h1:RXLPBG7B+hAViONVg0sHwK+U/ie1Y/NeXrq1JcARfoE=
+github.com/ory/x v0.0.93/go.mod h1:lfcTaGXpTZs7IEQAW00r9EtTCOxD//SiP5uWtNiz31g=
+github.com/ory/x v0.0.110/go.mod h1:DJfkE3GdakhshNhw4zlKoRaL/ozg/lcTahA9OCih2BE=
+github.com/ory/x v0.0.162 h1:xE/UBmmMlInTvlgGXUyo+VeZAcWU5gyWb/xh6jmBWsI=
+github.com/ory/x v0.0.162/go.mod h1:sj3z/MeCrAyNFFTfN6yK1nTmHXGSFnw+QwIIQ/Rowec=
github.com/otiai10/copy v1.5.1 h1:a/cs2E1/1V0az8K5nblbl+ymEa4E11AfaOLMar8V34w=
github.com/otiai10/copy v1.5.1/go.mod h1:XWfuS3CrI0R6IE0FbgHsEazaXO8G0LpMp9o8tos0x4E=
github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE=
@@ -378,20 +799,28 @@ github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT9
github.com/otiai10/mint v1.3.2 h1:VYWnrP5fXmz1MXvjuUvcBrXSjGE6xjON+axB/UrpO3E=
github.com/otiai10/mint v1.3.2/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc=
github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM=
+github.com/parnurzeal/gorequest v0.2.15/go.mod h1:3Kh2QUMJoqw3icWAecsyzkpY7UzRfDhbRdTjtNwNiUE=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
+github.com/pborman/uuid v1.2.0 h1:J7Q5mO4ysT1dv8hyrUGHb9+ooztCXu1D8MY8DZYsu3g=
github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
-github.com/pelletier/go-toml v1.4.0 h1:u3Z1r+oOXJIkxqw34zVhyPgjBsm6X2wn21NWs/HfSeg=
github.com/pelletier/go-toml v1.4.0/go.mod h1:PN7xzY2wHTK0K9p34ErDQMlFxa51Fk0OUruD3k1mMwo=
+github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys=
+github.com/pelletier/go-toml v1.8.0 h1:Keo9qb7iRJs2voHvunFtuuYFsbWeOBh8/P9v/kVMFtw=
+github.com/pelletier/go-toml v1.8.0/go.mod h1:D6yutnOGMveHEPV7VQOuvI/gXY61bv+9bAOTRnLElKs=
github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac=
+github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
github.com/philhofer/fwd v1.1.1 h1:GdGcTjf5RNAxwS4QLsiMzJYj5KEvPJD3Abr261yRQXQ=
github.com/philhofer/fwd v1.1.1/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
-github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
+github.com/pkg/errors v0.8.1-0.20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA=
+github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
@@ -414,59 +843,117 @@ github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y8
github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
+github.com/prometheus/procfs v0.0.0-20190425082905-87a4384529e0/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
+github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
+github.com/remyoudompheng/bigfft v0.0.0-20190728182440-6a916e37a237/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
+github.com/rogpeppe/go-internal v1.0.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
+github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
+github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
+github.com/rogpeppe/go-internal v1.3.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
+github.com/rogpeppe/go-internal v1.4.0/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
+github.com/rogpeppe/go-internal v1.5.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
+github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
+github.com/rubenv/sql-migrate v0.0.0-20190212093014-1007f53448d7/go.mod h1:WS0rl9eEliYI8DPnr3TOwz4439pay+qNgzJoVya/DmY=
+github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=
+github.com/santhosh-tekuri/jsonschema v1.2.4/go.mod h1:TEAUOeZSmIxTTuHatJzrvARHiuO9LYd+cIxzgEHCQI4=
+github.com/santhosh-tekuri/jsonschema/v2 v2.1.0/go.mod h1:yzJzKUGV4RbWqWIBBP4wSOBqavX5saE02yirLS0OTyg=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/savsgio/dictpool v0.0.0-20210404150759-6de1ea7c0e13 h1:GHosyqopD9q+RnHRd4HsqhT1873lF5necC/vB2oMcDI=
github.com/savsgio/dictpool v0.0.0-20210404150759-6de1ea7c0e13/go.mod h1:jYioskm8OBnvGMa37NVB/Ndy/ZiyCZouiEZlRl3T+wY=
github.com/savsgio/gotils v0.0.0-20210316171653-c54912823645 h1:ug9pfpEqVhvazyl1GezkQ9M/XdWsQn3VSx0s4qfH82I=
github.com/savsgio/gotils v0.0.0-20210316171653-c54912823645/go.mod h1:CN0/b5o0sSBi9K8fZTUamBG5NZXO0I64vTh9L3Mzhn0=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
+github.com/segmentio/analytics-go v3.0.1+incompatible/go.mod h1:C7CYBtQWk4vRk2RyLu0qOcbHJ18E3F1HV2C/8JvKN48=
+github.com/segmentio/analytics-go v3.1.0+incompatible/go.mod h1:C7CYBtQWk4vRk2RyLu0qOcbHJ18E3F1HV2C/8JvKN48=
+github.com/segmentio/backo-go v0.0.0-20160424052352-204274ad699c/go.mod h1:kJ9mm9YmoWSkk+oQ+5Cj8DEoRCX2JT6As4kEtIIOp1M=
+github.com/segmentio/backo-go v0.0.0-20200129164019-23eae7c10bd3/go.mod h1:9/Rh6yILuLysoQnZ2oNooD2g7aBnvM7r/fNVxRNWfBc=
+github.com/segmentio/conf v1.2.0/go.mod h1:Y3B9O/PqqWqjyxyWWseyj/quPEtMu1zDp/kVbSWWaB0=
+github.com/segmentio/go-snakecase v1.1.0/go.mod h1:jk1miR5MS7Na32PZUykG89Arm+1BUSYhuGR6b7+hJto=
+github.com/segmentio/objconv v1.0.1/go.mod h1:auayaH5k3137Cl4SoXTgrzQcuQDmvuVtZgS0fb1Ahys=
+github.com/serenize/snaker v0.0.0-20171204205717-a683aaf2d516/go.mod h1:Yow6lPLSAXx2ifx470yD/nUe22Dv5vBvxK/UK9UUTVs=
+github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
+github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc h1:jUIKcSPO9MoMJBbEoyE/RJoE8vz7Mb8AjvifMMwSyvY=
github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
+github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
+github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ=
+github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU=
+github.com/shurcooL/highlight_go v0.0.0-20170515013102-78fb10f4a5f8/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag=
+github.com/shurcooL/octicon v0.0.0-20180602230221-c42b0e3b24d9/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ=
+github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/simia-tech/crypt v0.5.0 h1:Y8xfAGqgd2wW2o4E63WIy9xr9w4jC1tDsOBHGKiqP0s=
github.com/simia-tech/crypt v0.5.0/go.mod h1:DMwvjPTzsiHrjqHVW5HvIbF4vUUzMCYDKVLsPWmLdTo=
+github.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
+github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
+github.com/sirupsen/logrus v1.1.0/go.mod h1:zrgwTnHtNr00buQ1vSptGe8m1f/BbgsPukg8qsT7A+A=
+github.com/sirupsen/logrus v1.1.1/go.mod h1:zrgwTnHtNr00buQ1vSptGe8m1f/BbgsPukg8qsT7A+A=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
+github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
+github.com/sirupsen/logrus v1.5.0/go.mod h1:+F7Ogzej0PZc/94MaYx/nvG9jOFMD2osvC3s+Squfpo=
+github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
+github.com/smartystreets/goconvey v0.0.0-20180222194500-ef6db91d284a/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s=
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
+github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE=
+github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA=
+github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72 h1:qLC7fQah7D6K1B0ujays3HV9gkFtllcxhzImRR7ArPQ=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
-github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
-github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
+github.com/spf13/afero v1.2.0/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
+github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
+github.com/spf13/afero v1.3.2 h1:GDarE4TJQI52kYSbSAmLiId1Elfj+xgSDqrUZxFhxlU=
+github.com/spf13/afero v1.3.2/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4=
+github.com/spf13/cast v1.2.0/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
+github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng=
+github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
+github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
+github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
+github.com/spf13/cobra v0.0.7/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
+github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
github.com/spf13/cobra v1.1.3 h1:xghbfqPkxzxP3C/f3n5DdpAbdKLj4ZE4BWQI362l53M=
github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo=
-github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
+github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
+github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
+github.com/spf13/pflag v1.0.1-0.20171106142849-4c012f6dcd95/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
+github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/spf13/viper v1.2.1/go.mod h1:P4AexN0a+C9tGAnUFNwDMYYZv3pjFuvmeiMyKRaNVlI=
+github.com/spf13/viper v1.3.1/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
+github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
+github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
github.com/spf13/viper v1.7.1 h1:pM5oEahlgWv/WnHXpgbKz7iLIxRf65tye2Ci+XFK5sk=
github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
+github.com/sqs/goreturns v0.0.0-20181028201513-538ac6014518/go.mod h1:CKI4AZ4XmGV240rTHfO0hfE83S6/a3/Q1siZJ/vXf7A=
github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI=
@@ -481,10 +968,16 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/subosito/gotenv v1.1.1/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/tebeka/selenium v0.9.9 h1:cNziB+etNgyH/7KlNI7RMC1ua5aH1+5wUlFQyzeMh+w=
github.com/tebeka/selenium v0.9.9/go.mod h1:5Fr8+pUvU6B1OiPfkdCKdXZyr5znvVkxuPd0NOdZCQc=
+github.com/tidwall/gjson v1.3.2/go.mod h1:P256ACg0Mn+j1RXIDXoss50DeIABTYK1PULOJHhxOls=
+github.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E=
+github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
+github.com/tidwall/sjson v1.0.4/go.mod h1:bURseu1nuBkFpIES5cz6zBtjmYeOQmEESshn7VpF15Y=
+github.com/tinylib/msgp v1.1.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
github.com/tinylib/msgp v1.1.5 h1:2gXmtWueD2HefZHQe1QOy9HVzmFrLOVvsXwXBQ0ayy0=
github.com/tinylib/msgp v1.1.5/go.mod h1:eQsjooMTnV42mHu917E26IogZ2930nFyBQdofk10Udg=
github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
@@ -492,16 +985,36 @@ github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1
github.com/tstranex/u2f v1.0.0 h1:HhJkSzDDlVSVIVt7pDJwCHQj67k7A5EeBgPmeD+pVsQ=
github.com/tstranex/u2f v1.0.0/go.mod h1:eahSLaqAS0zsIEv80+vXT7WanXs7MQQDg3j3wGBSayo=
github.com/ttacon/chalk v0.0.0-20160626202418-22c06c80ed31/go.mod h1:onvgF043R+lC5RZ8IT9rBXDaEDnpnw/Cl+HFiw+v/7Q=
+github.com/uber-go/atomic v1.3.2/go.mod h1:/Ct5t2lcmbJ4OSe/waGBoaVvVqtO0bmtfVNex1PFV8g=
+github.com/uber/jaeger-client-go v2.15.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk=
+github.com/uber/jaeger-client-go v2.22.1+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk=
+github.com/uber/jaeger-lib v1.5.0/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U=
+github.com/uber/jaeger-lib v2.2.0+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U=
+github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
+github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
+github.com/unrolled/secure v0.0.0-20180918153822-f340ee86eb8b/go.mod h1:mnPT77IAdsi/kV7+Es7y+pXALeV3h7G6dQF6mNYjcLA=
+github.com/unrolled/secure v0.0.0-20181005190816-ff9db2ff917f/go.mod h1:mnPT77IAdsi/kV7+Es7y+pXALeV3h7G6dQF6mNYjcLA=
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
+github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.24.0 h1:AAiG4oLDUArTb7rYf9oO2bkGooOqCaUF6a2u8asBP3I=
github.com/valyala/fasthttp v1.24.0/go.mod h1:0mw2RjXGOzxf4NL2jni3gUQ7LfjjUSiG5sskOUUSEpU=
github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio=
+github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
+github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
+github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c/go.mod h1:UrdRz5enIKZ63MEE3IF9l2/ebyx59GyGgPi+tICQdmM=
+github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
+github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0=
+go.elastic.co/apm v1.8.0/go.mod h1:tCw6CkOJgkWnzEthFN9HUP1uL3Gjc/Ur6m7gRPLaoH0=
+go.elastic.co/apm/module/apmhttp v1.8.0/go.mod h1:9LPFlEON51/lRbnWDfqAWErihIiAFDUMfMV27YjoWQ8=
+go.elastic.co/apm/module/apmot v1.8.0/go.mod h1:Q5Xzabte8G/fkvDjr1jlDuOSUt9hkVWNZEHh6ZNaTjI=
+go.elastic.co/fastjson v1.0.0/go.mod h1:PmeUOMMtLHQr9ZS9J9owrAVg0FkaZDRZJEFTTGHtchs=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg=
@@ -509,12 +1022,15 @@ go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
+go.opencensus.io v0.22.1/go.mod h1:Ap50jQcDJrx6rB6VgeeFPtuPIf3wMRvRfrfYDO6+BmA=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
+go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.13.0/go.mod h1:TwTkyRaTam1pOIb2wxcAiC2hkMVbokXkt6DEt5nDkD8=
go.opentelemetry.io/otel v0.13.0 h1:2isEnyzjjJZq6r2EKMsFj4TxiQiexsM04AVhwbR/oBA=
go.opentelemetry.io/otel v0.13.0/go.mod h1:dlSNewoRYikTkotEnxdmuBHgzT+k/idJSfDv/FxEnOY=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
+go.uber.org/atomic v1.5.1/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
@@ -523,28 +1039,54 @@ go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9E
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
+golang.org/x/crypto v0.0.0-20171113213409-9f005a07e0d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20180830192347-182538f80094/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20180910181607-0e37d006457b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20181001203147-e3636079e1a4/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20181009213950-7c1a557ab941/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20181015023909-0c41d7ab0a0e/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20181024171144-74cb1d3d52f4/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20181025113841-85e1b3f9139a/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20181025213731-e84da0312774/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20181106171534-e4dc69e5b2fd/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181112202954-3d3f9f413869/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20181127143415-eb0de9b17e85/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20190102171810-8d7daa0c54b3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20190103213133-ff983b9c42bc/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
+golang.org/x/crypto v0.0.0-20190422162423-af44ce270edf/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20191122220453-ac88ee75c92c/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20200117160349-530e935923ad/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20200320181102-891825fb96df/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 h1:It14KIkyBFYkHkwZ7k45minvA9aorojkyjGk9KJ5B/w=
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
+golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20190312203227-4b39c73a6495/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
+golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@@ -554,18 +1096,30 @@ golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHl
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/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20200130185559-910be7a94367/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=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
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.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-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180816102801-aaf60122140d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180921000356-2f5d2388922f/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180926154720-4dfa2610cdf3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181011144130-49bb7cea24b1/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181017193950-04a2e542c03f/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181102091132-c10e9556a7bc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181207154023-610586996380/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -574,35 +1128,66 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20191003171128-d98b1b443823/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200219183655-46282727080f/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/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-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226101413-39120d07d75e/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/oauth2 v0.0.0-20181003184128-c57b0facaced/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-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw=
+golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20180816055513-1c9583448a9c/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180831094639-fa5fdf94c789/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180906133057-8cf3aee42992/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180921163948-d47a0f339242/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180927150500-dad3d9fb7b6e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181005133103-4497e2df6f9e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181011152604-fa43e7bc11ba/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181022134430-8a28ead16f52/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181024145615-5cd93ef61a7c/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181025063200-d989b31c8746/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181026064943-731415f00dce/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181106135930-3a76605856fd/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116161606-93218def8b18/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181206074257-70b957f3b65e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190102155601-82a175fd1598/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190116161447-11f53e031339/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -611,6 +1196,7 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190515120540-06a5c4944438/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -618,13 +1204,22 @@ golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191025021431-6c3a3bfe00ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191105231009-c1f44814a5cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200121082415-34d275377bf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200720211630-cb9d2d5c5666/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073 h1:8qxJSnu+7dRq6upnbntrmriWByIakBuct5OM/MdQC1M=
@@ -643,9 +1238,33 @@ golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxb
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20181003024731-2f84ea8ef872/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20181006002542-f60d9635b16a/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20181008205924-a2b3f7f249e9/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20181013182035-5e66757b835f/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20181017214349-06f26fdaaa28/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20181024171208-a2dc47679d30/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20181026183834-f60e5f99f081/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20181105230042-78dc5bac0cac/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20181107215632-34b416bd17b3/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20181114190951-94339b83286c/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20181119130350-139d099f6620/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20181127195227-b4e97c0ed882/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20181127232545-e782529d0ddd/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20181203210056-e5f3ab76ea4b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20181205224935-3576414c54a4/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20181206194817-bcd4e47d0288/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20181207183836-8bc39b988060/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20181212172921-837e80568c09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190102213336-ca9055ed7d04/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190104182027-498d95493402/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190111214448-fc1d57b08d7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190118193359-16909d206f00/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
@@ -656,18 +1275,28 @@ golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBn
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190613204242-ed0dc450797f/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190624190245-7f2218787638/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190711191110-9a621aea19f8/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191004055002-72853e10c5a3/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/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191224055732-dd894d0a8a40/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200117220505-0cba7a3a9ee9/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200203215610-ab391d50b528/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200522201501-cb1345f3a375/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200721223218-6123e77877b2/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
+golang.org/x/tools v0.0.0-20201022035929-9cf592e881e9 h1:sEvmEcJVKBNUvgCUClbUQeHOAa9U0I2Ce1BooMvVCY4=
golang.org/x/tools v0.0.0-20201022035929-9cf592e881e9/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -676,6 +1305,12 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T
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=
+gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo=
+gonum.org/v1/gonum v0.6.2/go.mod h1:9mxDZsDKxgMAuccQkewq682L+0eCu4dCN2yonUJTCLU=
+gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
+gonum.org/v1/netlib v0.0.0-20191229114700-bbb4dff026f8/go.mod h1:2IgXn/sJaRbePPBA1wRj8OE+QLvVaH0q8SK6TSTKlnk=
+gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc=
+gonum.org/v1/plot v0.0.0-20200111075622-4abb28f724d5/go.mod h1:+HbaZVpsa73UwN7kXGCECULRHovLRJjH+t5cFPgxErs=
google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
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=
@@ -687,6 +1322,8 @@ google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7
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=
+google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM=
+google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
@@ -694,6 +1331,7 @@ google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRn
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s=
google.golang.org/genproto v0.0.0-20190626174449-989357319d63/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s=
+google.golang.org/genproto v0.0.0-20190708153700-3bdd9d9f5532/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
@@ -704,6 +1342,7 @@ google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLD
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
+google.golang.org/grpc v1.22.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
@@ -713,37 +1352,65 @@ google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
+google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+gopkg.in/DataDog/dd-trace-go.v1 v1.27.0/go.mod h1:Sp1lku8WJMvNV0kjDI4Ni/T7J/U3BO5ct5kEaoVU8+I=
+gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
+gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=
+gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
+gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
+gopkg.in/go-playground/mold.v2 v2.2.0/go.mod h1:XMyyRsGtakkDPbxXbrA5VODo6bUXyvoDjLd5l3T0XoA=
+gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw=
+gopkg.in/gorp.v1 v1.7.2/go.mod h1:Wo3h+DBQZIxATwftsglhdD/62zRFPhGhTiu5jUJmCaw=
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
-gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno=
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
+gopkg.in/ini.v1 v1.55.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
+gopkg.in/ini.v1 v1.57.0 h1:9unxIsFcTt4I55uWluz+UmL95q4kdJ0buvQ1ZIqVQww=
+gopkg.in/ini.v1 v1.57.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
+gopkg.in/mail.v2 v2.0.0-20180731213649-a0242b2233b4/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
+gopkg.in/square/go-jose.v2 v2.1.9/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
+gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
+gopkg.in/square/go-jose.v2 v2.5.1 h1:7odma5RETjNHWJnR32wx8t+Io4djHE1PqxCFx3iiZ2w=
+gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
+gopkg.in/validator.v2 v2.0.0-20180514200540-135c24b11c19/go.mod h1:o4V0GXN9/CAmCsvJ0oXYZvrZOe7syiDZSN1GWGZTGzc=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
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/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
+howett.net/plist v0.0.0-20181124034731-591f970eefbb/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0=
+modernc.org/cc v1.0.0/go.mod h1:1Sk4//wdnYJiUIxnW8ddKpaOJCF37yAdqYnkxUpaYxw=
+modernc.org/golex v1.0.0/go.mod h1:b/QX9oBD/LhixY6NDh+IdGv17hgB+51fET1i2kPSmvk=
+modernc.org/mathutil v1.0.0/go.mod h1:wU0vUrJsVWBZ4P6e7xtFJEhFSNsfRLJ8H458uRjg03k=
+modernc.org/strutil v1.1.0/go.mod h1:lstksw84oURvj9y3tn8lGvRxyRC1S2+g5uuIzNfIOBs=
+modernc.org/xc v1.0.0/go.mod h1:mRNCo0bvLjGhHO9WsyuKVU4q0ceiDDDoEeWDJHrNx8I=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
+rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU=
diff --git a/internal/authorization/util.go b/internal/authorization/util.go
index f2997b84..c258e441 100644
--- a/internal/authorization/util.go
+++ b/internal/authorization/util.go
@@ -5,6 +5,7 @@ import (
"regexp"
"strings"
+ "github.com/authelia/authelia/internal/authentication"
"github.com/authelia/authelia/internal/configuration/schema"
)
@@ -175,3 +176,17 @@ func domainToPrefixSuffix(domain string) (prefix, suffix string) {
return parts[0], strings.Join(parts[1:], ".")
}
+
+// IsAuthLevelSufficient returns true if the current authenticationLevel is above the authorizationLevel.
+func IsAuthLevelSufficient(authenticationLevel authentication.Level, authorizationLevel Level) bool {
+ switch authorizationLevel {
+ case Denied:
+ return false
+ case OneFactor:
+ return authenticationLevel >= authentication.OneFactor
+ case TwoFactor:
+ return authenticationLevel >= authentication.TwoFactor
+ }
+
+ return true
+}
diff --git a/internal/authorization/util_test.go b/internal/authorization/util_test.go
index db72d682..f06f6adb 100644
--- a/internal/authorization/util_test.go
+++ b/internal/authorization/util_test.go
@@ -7,6 +7,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
+ "github.com/authelia/authelia/internal/authentication"
"github.com/authelia/authelia/internal/configuration/schema"
)
@@ -182,3 +183,15 @@ func TestShouldParseACLNetworks(t *testing.T) {
assert.Equal(t, fourthNetwork, networksCacheMap["fec0::1"])
assert.Equal(t, fourthNetwork, networksCacheMap["fec0::1/128"])
}
+
+func TestShouldReturnCorrectValidationLevel(t *testing.T) {
+ assert.True(t, IsAuthLevelSufficient(authentication.NotAuthenticated, Bypass))
+ assert.True(t, IsAuthLevelSufficient(authentication.OneFactor, Bypass))
+ assert.True(t, IsAuthLevelSufficient(authentication.TwoFactor, Bypass))
+ assert.False(t, IsAuthLevelSufficient(authentication.NotAuthenticated, OneFactor))
+ assert.True(t, IsAuthLevelSufficient(authentication.OneFactor, OneFactor))
+ assert.True(t, IsAuthLevelSufficient(authentication.TwoFactor, OneFactor))
+ assert.False(t, IsAuthLevelSufficient(authentication.NotAuthenticated, TwoFactor))
+ assert.False(t, IsAuthLevelSufficient(authentication.OneFactor, TwoFactor))
+ assert.True(t, IsAuthLevelSufficient(authentication.TwoFactor, TwoFactor))
+}
diff --git a/internal/commands/certificates.go b/internal/commands/certificates.go
index d1d382b1..123df284 100644
--- a/internal/commands/certificates.go
+++ b/internal/commands/certificates.go
@@ -21,14 +21,14 @@ import (
)
var (
- host string
- validFrom string
- validFor time.Duration
- isCA bool
- rsaBits int
- ecdsaCurve string
- ed25519Key bool
- targetDirectory string
+ host string
+ validFrom string
+ validFor time.Duration
+ isCA bool
+ rsaBits int
+ ecdsaCurve string
+ ed25519Key bool
+ certificateTargetDirectory string
)
func init() {
@@ -45,7 +45,7 @@ func init() {
CertificatesGenerateCmd.PersistentFlags().IntVar(&rsaBits, "rsa-bits", 2048, "Size of RSA key to generate. Ignored if --ecdsa-curve is set")
CertificatesGenerateCmd.PersistentFlags().StringVar(&ecdsaCurve, "ecdsa-curve", "", "ECDSA curve to use to generate a key. Valid values are P224, P256 (recommended), P384, P521")
CertificatesGenerateCmd.PersistentFlags().BoolVar(&ed25519Key, "ed25519", false, "Generate an Ed25519 key")
- CertificatesGenerateCmd.PersistentFlags().StringVar(&targetDirectory, "dir", "", "Target directory where the certificate and keys will be stored")
+ CertificatesGenerateCmd.PersistentFlags().StringVar(&certificateTargetDirectory, "dir", "", "Target directory where the certificate and keys will be stored")
CertificatesCmd.AddCommand(CertificatesGenerateCmd)
}
@@ -144,7 +144,7 @@ func generateSelfSignedCertificate(cmd *cobra.Command, args []string) {
log.Fatalf("Failed to create certificate: %v", err)
}
- certPath := path.Join(targetDirectory, "cert.pem")
+ certPath := path.Join(certificateTargetDirectory, "cert.pem")
certOut, err := os.Create(certPath)
if err != nil {
@@ -161,7 +161,7 @@ func generateSelfSignedCertificate(cmd *cobra.Command, args []string) {
log.Printf("wrote %s\n", certPath)
- keyPath := path.Join(targetDirectory, "key.pem")
+ keyPath := path.Join(certificateTargetDirectory, "key.pem")
keyOut, err := os.OpenFile(keyPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
diff --git a/internal/commands/rsa.go b/internal/commands/rsa.go
new file mode 100644
index 00000000..a0d5d68f
--- /dev/null
+++ b/internal/commands/rsa.go
@@ -0,0 +1,79 @@
+package commands
+
+import (
+ "log"
+ "os"
+ "path"
+
+ "github.com/spf13/cobra"
+
+ "github.com/authelia/authelia/internal/utils"
+)
+
+var rsaTargetDirectory string
+
+func init() {
+ RSAGenerateCmd.PersistentFlags().StringVar(&rsaTargetDirectory, "dir", "", "Target directory where the keypair will be stored")
+
+ RSACmd.AddCommand(RSAGenerateCmd)
+}
+
+func generateRSAKeypair(cmd *cobra.Command, args []string) {
+ privateKey, publicKey := utils.GenerateRsaKeyPair(2048)
+
+ keyPath := path.Join(rsaTargetDirectory, "key.pem")
+ keyOut, err := os.OpenFile(keyPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
+
+ if err != nil {
+ log.Fatalf("Failed to open %s for writing: %v", keyPath, err)
+ return
+ }
+
+ _, err = keyOut.WriteString(utils.ExportRsaPrivateKeyAsPemStr(privateKey))
+ if err != nil {
+ log.Fatalf("Unable to write private key: %v", err)
+ return
+ }
+
+ if err := keyOut.Close(); err != nil {
+ log.Fatalf("Unable to close private key file: %v", err)
+ return
+ }
+
+ keyPath = path.Join(rsaTargetDirectory, "key.pub")
+ keyOut, err = os.OpenFile(keyPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
+
+ if err != nil {
+ log.Fatalf("Failed to open %s for writing: %v", keyPath, err)
+ return
+ }
+
+ publicPem, err := utils.ExportRsaPublicKeyAsPemStr(publicKey)
+ if err != nil {
+ log.Fatalf("Unable to marshal public key: %v", err)
+ }
+
+ _, err = keyOut.WriteString(publicPem)
+ if err != nil {
+ log.Fatalf("Unable to write private key: %v", err)
+ return
+ }
+
+ if err := keyOut.Close(); err != nil {
+ log.Fatalf("Unable to close public key file: %v", err)
+ return
+ }
+}
+
+// RSACmd RSA helper command.
+var RSACmd = &cobra.Command{
+ Use: "rsa",
+ Short: "Commands related to rsa keypair generation",
+}
+
+// RSAGenerateCmd certificate generation command.
+var RSAGenerateCmd = &cobra.Command{
+ Use: "generate",
+ Short: "Generate a RSA keypair",
+ Run: generateRSAKeypair,
+}
diff --git a/internal/configuration/config.template.yml b/internal/configuration/config.template.yml
index 154da0aa..b279bb11 100644
--- a/internal/configuration/config.template.yml
+++ b/internal/configuration/config.template.yml
@@ -553,4 +553,62 @@ notifier:
# sender: admin@example.com
# host: smtp.gmail.com
# port: 587
+
+##
+## Identity Providers
+##
+# identity_providers:
+
+ ##
+ ## OpenID Connect (Identity Provider)
+ ##
+ ## It's recommended you read the documentation before configuration of this section:
+ ## https://www.authelia.com/docs/configuration/identity-providers/oidc.html
+ # oidc:
+ ## The hmac_secret is used to sign OAuth2 tokens (authorization code, access tokens and refresh tokens).
+ ## HMAC Secret can also be set using a secret: https://www.authelia.com/docs/configuration/secrets.html
+ # hmac_secret: this_is_a_secret_abc123abc123abc
+
+ ## The issuer_private_key is used to sign the JWT forged by OpenID Connect.
+ ## Issuer Private Key can also be set using a secret: https://docs.authelia.com/configuration/secrets.html
+ # issuer_private_key: |
+ # --- KEY START
+ # --- KEY END
+
+ ## Clients is a list of known clients and their configuration.
+ # clients:
+ # -
+ ## The ID is the OpenID Connect ClientID which is used to link an application to a configuration.
+ # id: myapp
+
+ ## The description to show to users when they end up on the consent screen. Defaults to the ID above.
+ # description: My Application
+
+ ## The client secret is a shared secret between Authelia and the consumer of this client.
+ # secret: this_is_a_secret
+
+ ## The policy to require for this client; one_factor or two_factor.
+ # authorization_policy: two_factor
+
+ ## Redirect URI's specifies a list of valid case-sensitive callbacks for this client.
+ # redirect_uris:
+ # - https://oidc.example.com:8080/oauth2/callback
+
+ ## Scopes defines the valid scopes this client can request
+ # scopes:
+ # - openid
+ # - groups
+ # - email
+ # - profile
+
+ ## Grant Types configures which grants this client can obtain.
+ ## It's not recommended to define this unless you know what you're doing.
+ # grant_types:
+ # - refresh_token
+ # - "authorization_code
+
+ ## Response Types configures which responses this client can be sent.
+ ## It's not recommended to define this unless you know what you're doing.
+ # response_types:
+ # - code
...
diff --git a/internal/configuration/schema/configuration.go b/internal/configuration/schema/configuration.go
index 040b64a7..89434d35 100644
--- a/internal/configuration/schema/configuration.go
+++ b/internal/configuration/schema/configuration.go
@@ -14,6 +14,7 @@ type Configuration struct {
JWTSecret string `mapstructure:"jwt_secret"`
DefaultRedirectionURL string `mapstructure:"default_redirection_url"`
+ IdentityProviders IdentityProvidersConfiguration `mapstructure:"identity_providers"`
AuthenticationBackend AuthenticationBackendConfiguration `mapstructure:"authentication_backend"`
Session SessionConfiguration `mapstructure:"session"`
TOTP *TOTPConfiguration `mapstructure:"totp"`
diff --git a/internal/configuration/schema/identity_providers.go b/internal/configuration/schema/identity_providers.go
new file mode 100644
index 00000000..dd2d7519
--- /dev/null
+++ b/internal/configuration/schema/identity_providers.go
@@ -0,0 +1,35 @@
+package schema
+
+// IdentityProvidersConfiguration represents the IdentityProviders 2.0 configuration for Authelia.
+type IdentityProvidersConfiguration struct {
+ OIDC *OpenIDConnectConfiguration `mapstructure:"oidc"`
+}
+
+// OpenIDConnectConfiguration configuration for OpenID Connect.
+type OpenIDConnectConfiguration struct {
+ // This secret must be 32 bytes long
+ HMACSecret string `mapstructure:"hmac_secret"`
+ IssuerPrivateKey string `mapstructure:"issuer_private_key"`
+
+ Clients []OpenIDConnectClientConfiguration `mapstructure:"clients"`
+}
+
+// OpenIDConnectClientConfiguration configuration for an OpenID Connect client.
+type OpenIDConnectClientConfiguration struct {
+ ID string `mapstructure:"id"`
+ Description string `mapstructure:"description"`
+ Secret string `mapstructure:"secret"`
+ RedirectURIs []string `mapstructure:"redirect_uris"`
+ Policy string `mapstructure:"authorization_policy"`
+ Scopes []string `mapstructure:"scopes"`
+ GrantTypes []string `mapstructure:"grant_types"`
+ ResponseTypes []string `mapstructure:"response_types"`
+}
+
+// DefaultOpenIDConnectClientConfiguration contains defaults for OIDC AutheliaClients.
+var DefaultOpenIDConnectClientConfiguration = OpenIDConnectClientConfiguration{
+ Scopes: []string{"openid", "groups", "profile", "email"},
+ ResponseTypes: []string{"code"},
+ GrantTypes: []string{"refresh_token", "authorization_code"},
+ Policy: "two_factor",
+}
diff --git a/internal/configuration/validator/configuration.go b/internal/configuration/validator/configuration.go
index 3f6c6335..0bd640d2 100644
--- a/internal/configuration/validator/configuration.go
+++ b/internal/configuration/validator/configuration.go
@@ -91,4 +91,6 @@ func ValidateConfiguration(configuration *schema.Configuration, validator *schem
} else {
ValidateNotifier(configuration.Notifier, validator)
}
+
+ ValidateIdentityProviders(&configuration.IdentityProviders, validator)
}
diff --git a/internal/configuration/validator/const.go b/internal/configuration/validator/const.go
index 1014c5f7..fca945dd 100644
--- a/internal/configuration/validator/const.go
+++ b/internal/configuration/validator/const.go
@@ -7,6 +7,11 @@ const (
errFmtSessionRedisHostOrNodesRequired = "Either the host or a node must be provided when using the %s session provider"
errFmtReplacedConfigurationKey = "invalid configuration key '%s' was replaced by '%s'"
+ errOAuthOIDCServerClientRedirectURIFmt = "OIDC Server Client redirect URI %s has an invalid scheme %s, should be http or https"
+ errOAuthOIDCServerClientRedirectURICantBeParsedFmt = "OIDC Client with ID '%s' has an invalid redirect URI '%s' could not be parsed: %v"
+ errIdentityProvidersOIDCServerClientInvalidPolicyFmt = "OIDC Client with ID '%s' has an invalid policy '%s', should be either 'one_factor' or 'two_factor'"
+ errIdentityProvidersOIDCServerClientInvalidSecFmt = "OIDC Client with ID '%s' has an empty secret"
+
errFileHashing = "config key incorrect: authentication_backend.file.hashing should be authentication_backend.file.password"
errFilePHashing = "config key incorrect: authentication_backend.file.password_hashing should be authentication_backend.file.password"
errFilePOptions = "config key incorrect: authentication_backend.file.password_options should be authentication_backend.file.password"
@@ -42,15 +47,17 @@ var validRequestMethods = []string{"GET", "HEAD", "POST", "PUT", "PATCH", "DELET
// SecretNames contains a map of secret names.
var SecretNames = map[string]string{
- "JWTSecret": "jwt_secret",
- "SessionSecret": "session.secret",
- "DUOSecretKey": "duo_api.secret_key",
- "RedisPassword": "session.redis.password",
- "RedisSentinelPassword": "session.redis.high_availability.sentinel_password",
- "LDAPPassword": "authentication_backend.ldap.password",
- "SMTPPassword": "notifier.smtp.password",
- "MySQLPassword": "storage.mysql.password",
- "PostgreSQLPassword": "storage.postgres.password",
+ "JWTSecret": "jwt_secret",
+ "SessionSecret": "session.secret",
+ "DUOSecretKey": "duo_api.secret_key",
+ "RedisPassword": "session.redis.password",
+ "RedisSentinelPassword": "session.redis.high_availability.sentinel_password",
+ "LDAPPassword": "authentication_backend.ldap.password",
+ "SMTPPassword": "notifier.smtp.password",
+ "MySQLPassword": "storage.mysql.password",
+ "PostgreSQLPassword": "storage.postgres.password",
+ "OpenIDConnectHMACSecret": "identity_providers.oidc.hmac_secret",
+ "OpenIDConnectIssuerPrivateKey": "identity_providers.oidc.issuer_private_key",
}
// validKeys is a list of valid keys that are not secret names. For the sake of consistency please place any secret in
@@ -184,6 +191,9 @@ var validKeys = []string{
"authentication_backend.file.password.salt_length",
"authentication_backend.file.password.memory",
"authentication_backend.file.password.parallelism",
+
+ // Identity Provider Keys.
+ "identity_providers.oidc.clients",
}
var replacedKeys = map[string]string{
diff --git a/internal/configuration/validator/identity_providers.go b/internal/configuration/validator/identity_providers.go
new file mode 100644
index 00000000..792621bb
--- /dev/null
+++ b/internal/configuration/validator/identity_providers.go
@@ -0,0 +1,98 @@
+package validator
+
+import (
+ "fmt"
+ "net/url"
+
+ "github.com/authelia/authelia/internal/configuration/schema"
+ "github.com/authelia/authelia/internal/utils"
+)
+
+// ValidateIdentityProviders validates and update IdentityProviders configuration.
+func ValidateIdentityProviders(configuration *schema.IdentityProvidersConfiguration, validator *schema.StructValidator) {
+ validateOIDC(configuration.OIDC, validator)
+}
+
+func validateOIDC(configuration *schema.OpenIDConnectConfiguration, validator *schema.StructValidator) {
+ if configuration != nil {
+ if configuration.IssuerPrivateKey == "" {
+ validator.Push(fmt.Errorf("OIDC Server issuer private key must be provided"))
+ }
+
+ validateOIDCClients(configuration, validator)
+
+ if len(configuration.Clients) == 0 {
+ validator.Push(fmt.Errorf("OIDC Server has no clients defined"))
+ }
+ }
+}
+
+func validateOIDCClients(configuration *schema.OpenIDConnectConfiguration, validator *schema.StructValidator) {
+ invalidID, duplicateIDs := false, false
+
+ var ids []string
+
+ for c, client := range configuration.Clients {
+ if client.ID == "" {
+ invalidID = true
+ } else {
+ if client.Description == "" {
+ configuration.Clients[c].Description = client.ID
+ }
+
+ if utils.IsStringInSliceFold(client.ID, ids) {
+ duplicateIDs = true
+ }
+ ids = append(ids, client.ID)
+ }
+
+ if client.Secret == "" {
+ validator.Push(fmt.Errorf(errIdentityProvidersOIDCServerClientInvalidSecFmt, client.ID))
+ }
+
+ if client.Policy == "" {
+ configuration.Clients[c].Policy = schema.DefaultOpenIDConnectClientConfiguration.Policy
+ } else if client.Policy != oneFactorPolicy && client.Policy != twoFactorPolicy {
+ validator.Push(fmt.Errorf(errIdentityProvidersOIDCServerClientInvalidPolicyFmt, client.ID, client.Policy))
+ }
+
+ if len(client.Scopes) == 0 {
+ configuration.Clients[c].Scopes = schema.DefaultOpenIDConnectClientConfiguration.Scopes
+ } else if !utils.IsStringInSlice("openid", client.Scopes) {
+ configuration.Clients[c].Scopes = append(configuration.Clients[c].Scopes, "openid")
+ }
+
+ if len(client.GrantTypes) == 0 {
+ configuration.Clients[c].GrantTypes = schema.DefaultOpenIDConnectClientConfiguration.GrantTypes
+ }
+
+ if len(client.ResponseTypes) == 0 {
+ configuration.Clients[c].ResponseTypes = schema.DefaultOpenIDConnectClientConfiguration.ResponseTypes
+ }
+
+ validateOIDCClientRedirectURIs(client, validator)
+ }
+
+ if invalidID {
+ validator.Push(fmt.Errorf("OIDC Server has one or more clients with an empty ID"))
+ }
+
+ if duplicateIDs {
+ validator.Push(fmt.Errorf("OIDC Server has clients with duplicate ID's"))
+ }
+}
+
+func validateOIDCClientRedirectURIs(client schema.OpenIDConnectClientConfiguration, validator *schema.StructValidator) {
+ for _, redirectURI := range client.RedirectURIs {
+ parsedURI, err := url.Parse(redirectURI)
+
+ if err != nil {
+ validator.Push(fmt.Errorf(errOAuthOIDCServerClientRedirectURICantBeParsedFmt, client.ID, redirectURI, err))
+ break
+ }
+
+ if parsedURI.Scheme != "https" && parsedURI.Scheme != "http" {
+ validator.Push(fmt.Errorf(errOAuthOIDCServerClientRedirectURIFmt, redirectURI, parsedURI.Scheme))
+ }
+ }
+}
diff --git a/internal/configuration/validator/identity_providers_test.go b/internal/configuration/validator/identity_providers_test.go
new file mode 100644
index 00000000..00e50272
--- /dev/null
+++ b/internal/configuration/validator/identity_providers_test.go
@@ -0,0 +1,172 @@
+package validator
+
+import (
+ "errors"
+ "fmt"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/authelia/authelia/internal/configuration/schema"
+)
+
+func TestShouldRaiseErrorWhenInvalidOIDCServerConfiguration(t *testing.T) {
+ validator := schema.NewStructValidator()
+ config := &schema.IdentityProvidersConfiguration{
+ OIDC: &schema.OpenIDConnectConfiguration{
+ HMACSecret: "abc",
+ IssuerPrivateKey: "",
+ },
+ }
+
+ ValidateIdentityProviders(config, validator)
+
+ require.Len(t, validator.Errors(), 2)
+
+ assert.EqualError(t, validator.Errors()[0], "OIDC Server issuer private key must be provided")
+ assert.EqualError(t, validator.Errors()[1], "OIDC Server has no clients defined")
+}
+
+func TestShouldRaiseErrorWhenOIDCServerIssuerPrivateKeyPathInvalid(t *testing.T) {
+ validator := schema.NewStructValidator()
+ config := &schema.IdentityProvidersConfiguration{
+ OIDC: &schema.OpenIDConnectConfiguration{
+ HMACSecret: "rLABDrx87et5KvRHVUgTm3pezWWd8LMN",
+ IssuerPrivateKey: "key-material",
+ },
+ }
+
+ ValidateIdentityProviders(config, validator)
+
+ require.Len(t, validator.Errors(), 1)
+
+ assert.EqualError(t, validator.Errors()[0], "OIDC Server has no clients defined")
+}
+
+func TestShouldRaiseErrorWhenOIDCServerClientBadValues(t *testing.T) {
+ validator := schema.NewStructValidator()
+ config := &schema.IdentityProvidersConfiguration{
+ OIDC: &schema.OpenIDConnectConfiguration{
+ HMACSecret: "rLABDrx87et5KvRHVUgTm3pezWWd8LMN",
+ IssuerPrivateKey: "key-material",
+ Clients: []schema.OpenIDConnectClientConfiguration{
+ {
+ ID: "",
+ Secret: "",
+ Policy: "",
+ RedirectURIs: []string{
+ "tcp://google.com",
+ },
+ },
+ {
+ ID: "a-client",
+ Secret: "a-secret",
+ Policy: "a-policy",
+ RedirectURIs: []string{
+ "https://google.com",
+ },
+ },
+ {
+ ID: "a-client",
+ Secret: "a-secret",
+ Policy: "a-policy",
+ RedirectURIs: []string{
+ "https://google.com",
+ },
+ },
+ {
+ ID: "client-check-uri-parse",
+ Secret: "a-secret",
+ Policy: twoFactorPolicy,
+ RedirectURIs: []string{
+ "http://abc@%two",
+ },
+ },
+ },
+ },
+ }
+
+ ValidateIdentityProviders(config, validator)
+
+ require.Len(t, validator.Errors(), 7)
+
+ assert.Equal(t, schema.DefaultOpenIDConnectClientConfiguration.Policy, config.OIDC.Clients[0].Policy)
+ assert.EqualError(t, validator.Errors()[0], fmt.Sprintf(errIdentityProvidersOIDCServerClientInvalidSecFmt, ""))
+ assert.EqualError(t, validator.Errors()[1], fmt.Sprintf(errOAuthOIDCServerClientRedirectURIFmt, "tcp://google.com", "tcp"))
+ assert.EqualError(t, validator.Errors()[2], fmt.Sprintf(errIdentityProvidersOIDCServerClientInvalidPolicyFmt, "a-client", "a-policy"))
+ assert.EqualError(t, validator.Errors()[3], fmt.Sprintf(errIdentityProvidersOIDCServerClientInvalidPolicyFmt, "a-client", "a-policy"))
+ assert.EqualError(t, validator.Errors()[4], fmt.Sprintf(errOAuthOIDCServerClientRedirectURICantBeParsedFmt, "client-check-uri-parse", "http://abc@%two", errors.New("parse \"http://abc@%two\": invalid URL escape \"%tw\"")))
+ assert.EqualError(t, validator.Errors()[5], "OIDC Server has one or more clients with an empty ID")
+ assert.EqualError(t, validator.Errors()[6], "OIDC Server has clients with duplicate ID's")
+}
+
+func TestShouldNotRaiseErrorWhenOIDCServerConfiguredCorrectly(t *testing.T) {
+ validator := schema.NewStructValidator()
+ config := &schema.IdentityProvidersConfiguration{
+ OIDC: &schema.OpenIDConnectConfiguration{
+ HMACSecret: "rLABDrx87et5KvRHVUgTm3pezWWd8LMN",
+ IssuerPrivateKey: "../../../README.md",
+ Clients: []schema.OpenIDConnectClientConfiguration{
+ {
+ ID: "a-client",
+ Secret: "a-client-secret",
+ Policy: oneFactorPolicy,
+ RedirectURIs: []string{
+ "https://google.com",
+ },
+ },
+ {
+ ID: "b-client",
+ Description: "Normal Description",
+ Secret: "b-client-secret",
+ Policy: oneFactorPolicy,
+ RedirectURIs: []string{
+ "https://google.com",
+ },
+ Scopes: []string{
+ "groups",
+ },
+ GrantTypes: []string{
+ "refresh_token",
+ },
+ ResponseTypes: []string{
+ "token",
+ "code",
+ },
+ },
+ },
+ },
+ }
+
+ ValidateIdentityProviders(config, validator)
+
+ assert.Len(t, validator.Errors(), 0)
+
+ assert.Equal(t, config.OIDC.Clients[0].ID, config.OIDC.Clients[0].Description)
+ assert.Equal(t, "Normal Description", config.OIDC.Clients[1].Description)
+
+ require.Len(t, config.OIDC.Clients[0].Scopes, 4)
+ assert.Equal(t, "openid", config.OIDC.Clients[0].Scopes[0])
+ assert.Equal(t, "groups", config.OIDC.Clients[0].Scopes[1])
+ assert.Equal(t, "profile", config.OIDC.Clients[0].Scopes[2])
+ assert.Equal(t, "email", config.OIDC.Clients[0].Scopes[3])
+
+ require.Len(t, config.OIDC.Clients[0].GrantTypes, 2)
+ assert.Equal(t, "refresh_token", config.OIDC.Clients[0].GrantTypes[0])
+ assert.Equal(t, "authorization_code", config.OIDC.Clients[0].GrantTypes[1])
+
+ require.Len(t, config.OIDC.Clients[0].ResponseTypes, 1)
+ assert.Equal(t, "code", config.OIDC.Clients[0].ResponseTypes[0])
+
+ require.Len(t, config.OIDC.Clients[1].Scopes, 2)
+ assert.Equal(t, "groups", config.OIDC.Clients[1].Scopes[0])
+ assert.Equal(t, "openid", config.OIDC.Clients[1].Scopes[1])
+
+ require.Len(t, config.OIDC.Clients[1].GrantTypes, 1)
+ assert.Equal(t, "refresh_token", config.OIDC.Clients[1].GrantTypes[0])
+
+ require.Len(t, config.OIDC.Clients[1].ResponseTypes, 2)
+ assert.Equal(t, "token", config.OIDC.Clients[1].ResponseTypes[0])
+ assert.Equal(t, "code", config.OIDC.Clients[1].ResponseTypes[1])
+}
diff --git a/internal/configuration/validator/secrets.go b/internal/configuration/validator/secrets.go
index cb46513d..285c413a 100644
--- a/internal/configuration/validator/secrets.go
+++ b/internal/configuration/validator/secrets.go
@@ -58,6 +58,11 @@ func ValidateSecrets(configuration *schema.Configuration, validator *schema.Stru
if configuration.Storage.PostgreSQL != nil {
configuration.Storage.PostgreSQL.Password = getSecretValue(SecretNames["PostgreSQLPassword"], validator, viper)
}
+
+ if configuration.IdentityProviders.OIDC != nil {
+ configuration.IdentityProviders.OIDC.HMACSecret = getSecretValue(SecretNames["OpenIDConnectHMACSecret"], validator, viper)
+ configuration.IdentityProviders.OIDC.IssuerPrivateKey = getSecretValue(SecretNames["OpenIDConnectIssuerPrivateKey"], validator, viper)
+ }
}
func getSecretValue(name string, validator *schema.StructValidator, viper *viper.Viper) string {
@@ -75,7 +80,8 @@ func getSecretValue(name string, validator *schema.StructValidator, viper *viper
if err != nil {
validator.Push(fmt.Errorf("error loading secret file (%s): %s", name, err))
} else {
- return strings.ReplaceAll(string(content), "\n", "")
+ // TODO: Test this functionality.
+ return strings.TrimRight(string(content), "\n")
}
}
diff --git a/internal/handlers/const.go b/internal/handlers/const.go
index c3f3701e..5a076b1b 100644
--- a/internal/handlers/const.go
+++ b/internal/handlers/const.go
@@ -25,8 +25,6 @@ const remoteNameHeader = "Remote-Name"
const remoteEmailHeader = "Remote-Email"
const remoteGroupsHeader = "Remote-Groups"
-var protoHostSeparator = []byte("://")
-
const (
// Forbidden means the user is forbidden the access to a resource.
Forbidden authorizationMatching = iota
@@ -57,3 +55,30 @@ const testUsername = "john"
const movingAverageWindow = 10
const msMinimumDelay1FA = float64(250)
const msMaximumRandomDelay = int64(85)
+
+// OIDC constants.
+const (
+ oidcWellKnownPath = "/.well-known/openid-configuration"
+ oidcJWKsPath = "/api/oidc/jwks"
+ oidcAuthorizePath = "/api/oidc/authorize"
+ oidcTokenPath = "/api/oidc/token" //nolint:gosec // This is not a hard coded credential, it's a path.
+ oidcIntrospectPath = "/api/oidc/introspect"
+ oidcRevokePath = "/api/oidc/revoke"
+
+ // Note: If you change this const you must also do so in the frontend at web/src/services/Api.ts.
+ oidcConsentPath = "/api/oidc/consent"
+)
+
+const (
+ accept = "accept"
+ reject = "reject"
+)
+
+var scopeDescriptions = map[string]string{
+ "openid": "Use OpenID to verify your identity",
+ "email": "Access your email addresses",
+ "profile": "Access your username",
+ "groups": "Access your group membership",
+}
+
+var audienceDescriptions = map[string]string{}
diff --git a/internal/handlers/handler_firstfactor.go b/internal/handlers/handler_firstfactor.go
index 56201a8f..39b02cc4 100644
--- a/internal/handlers/handler_firstfactor.go
+++ b/internal/handlers/handler_firstfactor.go
@@ -127,8 +127,12 @@ func FirstFactorPost(msInitialDelay time.Duration, delayEnabled bool) middleware
ctx.Logger.Debugf("Credentials validation of user %s is ok", bodyJSON.Username)
- // Reset all values from previous session before regenerating the cookie.
- err = ctx.SaveSession(session.NewDefaultUserSession())
+ userSession := ctx.GetSession()
+ newSession := session.NewDefaultUserSession()
+ newSession.OIDCWorkflowSession = userSession.OIDCWorkflowSession
+
+ // Reset all values from previous session except OIDC workflow before regenerating the cookie.
+ err = ctx.SaveSession(newSession)
if err != nil {
handleAuthenticationUnauthorized(ctx, fmt.Errorf("Unable to reset the session for user %s: %s", bodyJSON.Username, err.Error()), authenticationFailedMessage)
@@ -165,7 +169,6 @@ func FirstFactorPost(msInitialDelay time.Duration, delayEnabled bool) middleware
ctx.Logger.Tracef("Details for user %s => groups: %s, emails %s", bodyJSON.Username, userDetails.Groups, userDetails.Emails)
// And set those information in the new session.
- userSession := ctx.GetSession()
userSession.Username = userDetails.Username
userSession.DisplayName = userDetails.DisplayName
userSession.Groups = userDetails.Groups
@@ -188,6 +191,10 @@ func FirstFactorPost(msInitialDelay time.Duration, delayEnabled bool) middleware
successful = true
- Handle1FAResponse(ctx, bodyJSON.TargetURL, bodyJSON.RequestMethod, userSession.Username, userSession.Groups)
+ if userSession.OIDCWorkflowSession != nil {
+ HandleOIDCWorkflowResponse(ctx)
+ } else {
+ Handle1FAResponse(ctx, bodyJSON.TargetURL, bodyJSON.RequestMethod, userSession.Username, userSession.Groups)
+ }
}
}
diff --git a/internal/handlers/handler_oidc_authorize.go b/internal/handlers/handler_oidc_authorize.go
new file mode 100644
index 00000000..3220949b
--- /dev/null
+++ b/internal/handlers/handler_oidc_authorize.go
@@ -0,0 +1,129 @@
+package handlers
+
+import (
+ "fmt"
+ "net/http"
+ "strings"
+
+ "github.com/ory/fosite"
+
+ "github.com/authelia/authelia/internal/logging"
+ "github.com/authelia/authelia/internal/middlewares"
+ "github.com/authelia/authelia/internal/oidc"
+ "github.com/authelia/authelia/internal/session"
+)
+
+func oidcAuthorize(ctx *middlewares.AutheliaCtx, rw http.ResponseWriter, r *http.Request) {
+ ar, err := ctx.Providers.OpenIDConnect.Fosite.NewAuthorizeRequest(ctx, r)
+ if err != nil {
+ logging.Logger().Errorf("Error occurred in NewAuthorizeRequest: %+v", err)
+ ctx.Providers.OpenIDConnect.Fosite.WriteAuthorizeError(rw, ar, err)
+
+ return
+ }
+
+ clientID := ar.GetClient().GetID()
+ client, err := ctx.Providers.OpenIDConnect.Store.GetInternalClient(clientID)
+
+ if err != nil {
+ err := fmt.Errorf("Unable to find related client configuration with name '%s': %v", ar.GetID(), err)
+ ctx.Logger.Error(err)
+ ctx.Providers.OpenIDConnect.Fosite.WriteAuthorizeError(rw, ar, err)
+
+ return
+ }
+
+ userSession := ctx.GetSession()
+
+ requestedScopes := ar.GetRequestedScopes()
+ requestedAudience := ar.GetRequestedAudience()
+
+ isAuthInsufficient := !client.IsAuthenticationLevelSufficient(userSession.AuthenticationLevel)
+
+ if isAuthInsufficient || (isConsentMissing(userSession.OIDCWorkflowSession, requestedScopes, requestedAudience)) {
+ oidcAuthorizeHandleAuthorizationOrConsentInsufficient(ctx, userSession, client, isAuthInsufficient, rw, r, ar)
+
+ return
+ }
+
+ for _, scope := range requestedScopes {
+ ar.GrantScope(scope)
+ }
+
+ for _, a := range requestedAudience {
+ ar.GrantAudience(a)
+ }
+
+ userSession.OIDCWorkflowSession = nil
+ if err := ctx.SaveSession(userSession); err != nil {
+ ctx.Logger.Errorf("%v", err)
+ http.Error(rw, err.Error(), http.StatusInternalServerError)
+
+ return
+ }
+
+ oauthSession, err := newOIDCSession(ctx, ar)
+ if err != nil {
+ ctx.Logger.Errorf("Error occurred in NewOIDCSession: %+v", err)
+ ctx.Providers.OpenIDConnect.Fosite.WriteAuthorizeError(rw, ar, err)
+
+ return
+ }
+
+ response, err := ctx.Providers.OpenIDConnect.Fosite.NewAuthorizeResponse(ctx, ar, oauthSession)
+ if err != nil {
+ ctx.Logger.Errorf("Error occurred in NewAuthorizeResponse: %+v", err)
+ ctx.Providers.OpenIDConnect.Fosite.WriteAuthorizeError(rw, ar, err)
+
+ return
+ }
+
+ ctx.Providers.OpenIDConnect.Fosite.WriteAuthorizeResponse(rw, ar, response)
+}
+
+func oidcAuthorizeHandleAuthorizationOrConsentInsufficient(
+ ctx *middlewares.AutheliaCtx, userSession session.UserSession, client *oidc.InternalClient, isAuthInsufficient bool,
+ rw http.ResponseWriter, r *http.Request,
+ ar fosite.AuthorizeRequester) {
+ forwardedProtoHost, err := ctx.ForwardedProtoHost()
+ if err != nil {
+ ctx.Logger.Errorf("%v", err)
+ http.Error(rw, err.Error(), http.StatusBadRequest)
+
+ return
+ }
+
+ redirectURL := fmt.Sprintf("%s%s", forwardedProtoHost, string(ctx.Request.RequestURI()))
+
+ ctx.Logger.Debugf("User %s must consent with scopes %s",
+ userSession.Username, strings.Join(ar.GetRequestedScopes(), ", "))
+
+ userSession.OIDCWorkflowSession = new(session.OIDCWorkflowSession)
+ userSession.OIDCWorkflowSession.ClientID = client.ID
+ userSession.OIDCWorkflowSession.RequestedScopes = ar.GetRequestedScopes()
+ userSession.OIDCWorkflowSession.RequestedAudience = ar.GetRequestedAudience()
+ userSession.OIDCWorkflowSession.AuthURI = redirectURL
+ userSession.OIDCWorkflowSession.TargetURI = ar.GetRedirectURI().String()
+ userSession.OIDCWorkflowSession.RequiredAuthorizationLevel = client.Policy
+
+ if err := ctx.SaveSession(userSession); err != nil {
+ ctx.Logger.Errorf("%v", err)
+ http.Error(rw, err.Error(), http.StatusInternalServerError)
+
+ return
+ }
+
+ uri, err := ctx.ForwardedProtoHost()
+ if err != nil {
+ ctx.Logger.Errorf("%v", err)
+ http.Error(rw, err.Error(), http.StatusBadRequest)
+
+ return
+ }
+
+ if isAuthInsufficient {
+ http.Redirect(rw, r, uri, http.StatusFound)
+ } else {
+ http.Redirect(rw, r, fmt.Sprintf("%s/consent", uri), http.StatusFound)
+ }
+}
diff --git a/internal/handlers/handler_oidc_consent.go b/internal/handlers/handler_oidc_consent.go
new file mode 100644
index 00000000..b96d6840
--- /dev/null
+++ b/internal/handlers/handler_oidc_consent.go
@@ -0,0 +1,124 @@
+package handlers
+
+import (
+ "encoding/json"
+ "fmt"
+
+ "github.com/authelia/authelia/internal/middlewares"
+)
+
+func oidcConsent(ctx *middlewares.AutheliaCtx) {
+ userSession := ctx.GetSession()
+
+ if userSession.OIDCWorkflowSession == nil {
+ ctx.Logger.Debugf("Cannot consent for user %s when OIDC workflow has not been initiated", userSession.Username)
+ ctx.ReplyForbidden()
+
+ return
+ }
+
+ clientID := userSession.OIDCWorkflowSession.ClientID
+ client, err := ctx.Providers.OpenIDConnect.Store.GetInternalClient(clientID)
+
+ if err != nil {
+ ctx.Logger.Debugf("Unable to find related client configuration with name '%s': %v", clientID, err)
+ ctx.ReplyForbidden()
+
+ return
+ }
+
+ if !client.IsAuthenticationLevelSufficient(userSession.AuthenticationLevel) {
+ ctx.Logger.Debugf("Insufficient permissions to give consent v2 %d -> %d", userSession.AuthenticationLevel, userSession.OIDCWorkflowSession.RequiredAuthorizationLevel)
+ ctx.ReplyForbidden()
+
+ return
+ }
+
+ var body ConsentGetResponseBody
+ body.Scopes = scopeNamesToScopes(userSession.OIDCWorkflowSession.RequestedScopes)
+ body.Audience = audienceNamesToAudience(userSession.OIDCWorkflowSession.RequestedAudience)
+ body.ClientID = client.ID
+ body.ClientDescription = client.Description
+
+ if err := ctx.SetJSONBody(body); err != nil {
+ ctx.Error(fmt.Errorf("Unable to set JSON body: %v", err), "Operation failed")
+ }
+}
+
+func oidcConsentPOST(ctx *middlewares.AutheliaCtx) {
+ userSession := ctx.GetSession()
+
+ if userSession.OIDCWorkflowSession == nil {
+ ctx.Logger.Debugf("Cannot consent for user %s when OIDC workflow has not been initiated", userSession.Username)
+ ctx.ReplyForbidden()
+
+ return
+ }
+
+ client, err := ctx.Providers.OpenIDConnect.Store.GetInternalClient(userSession.OIDCWorkflowSession.ClientID)
+
+ if err != nil {
+ ctx.Logger.Debugf("Unable to find related client configuration with name '%s': %v", userSession.OIDCWorkflowSession.ClientID, err)
+ ctx.ReplyForbidden()
+
+ return
+ }
+
+ if !client.IsAuthenticationLevelSufficient(userSession.AuthenticationLevel) {
+ ctx.Logger.Debugf("Insufficient permissions to give consent v1 %d -> %d", userSession.AuthenticationLevel, userSession.OIDCWorkflowSession.RequiredAuthorizationLevel)
+ ctx.ReplyForbidden()
+
+ return
+ }
+
+ var body ConsentPostRequestBody
+ err = json.Unmarshal(ctx.Request.Body(), &body)
+
+ if err != nil {
+ ctx.Error(fmt.Errorf("Unable to unmarshal body: %v", err), "Operation failed")
+ return
+ }
+
+ if body.AcceptOrReject != accept && body.AcceptOrReject != reject {
+ ctx.Logger.Infof("User %s tried to reply to consent with an unexpected verb", userSession.Username)
+ ctx.ReplyBadRequest()
+
+ return
+ }
+
+ if userSession.OIDCWorkflowSession.ClientID != body.ClientID {
+ ctx.Logger.Infof("User %s consented to scopes of another client (%s) than expected (%s). Beware this can be a sign of attack",
+ userSession.Username, body.ClientID, userSession.OIDCWorkflowSession.ClientID)
+ ctx.ReplyBadRequest()
+
+ return
+ }
+
+ var redirectionURL string
+
+ if body.AcceptOrReject == accept {
+ redirectionURL = userSession.OIDCWorkflowSession.AuthURI
+ userSession.OIDCWorkflowSession.GrantedScopes = userSession.OIDCWorkflowSession.RequestedScopes
+ userSession.OIDCWorkflowSession.GrantedAudience = userSession.OIDCWorkflowSession.RequestedAudience
+
+ if err := ctx.SaveSession(userSession); err != nil {
+ ctx.Error(fmt.Errorf("Unable to write session: %v", err), "Operation failed")
+ return
+ }
+ } else if body.AcceptOrReject == reject {
+ redirectionURL = fmt.Sprintf("%s?error=access_denied&error_description=%s",
+ userSession.OIDCWorkflowSession.TargetURI, "User has rejected the scopes")
+ userSession.OIDCWorkflowSession = nil
+
+ if err := ctx.SaveSession(userSession); err != nil {
+ ctx.Error(fmt.Errorf("Unable to write session: %v", err), "Operation failed")
+ return
+ }
+ }
+
+ response := ConsentPostResponseBody{RedirectURI: redirectionURL}
+
+ if err := ctx.SetJSONBody(response); err != nil {
+ ctx.Error(fmt.Errorf("Unable to set JSON body in response"), "Operation failed")
+ }
+}
diff --git a/internal/handlers/handler_oidc_introspect.go b/internal/handlers/handler_oidc_introspect.go
new file mode 100644
index 00000000..6873c8d4
--- /dev/null
+++ b/internal/handlers/handler_oidc_introspect.go
@@ -0,0 +1,29 @@
+package handlers
+
+import (
+ "net/http"
+
+ "github.com/authelia/authelia/internal/middlewares"
+)
+
+func oidcIntrospect(ctx *middlewares.AutheliaCtx, rw http.ResponseWriter, req *http.Request) {
+ oidcSession, err := newDefaultOIDCSession(ctx)
+
+ if err != nil {
+ ctx.Logger.Errorf("Error occurred in NewDefaultOIDCSession: %+v", err)
+ ctx.Providers.OpenIDConnect.Fosite.WriteIntrospectionError(rw, err)
+
+ return
+ }
+
+ ir, err := ctx.Providers.OpenIDConnect.Fosite.NewIntrospectionRequest(ctx, req, oidcSession)
+
+ if err != nil {
+ ctx.Logger.Errorf("Error occurred in NewIntrospectionRequest: %+v", err)
+ ctx.Providers.OpenIDConnect.Fosite.WriteIntrospectionError(rw, err)
+
+ return
+ }
+
+ ctx.Providers.OpenIDConnect.Fosite.WriteIntrospectionResponse(rw, ir)
+}
diff --git a/internal/handlers/handler_oidc_jwks.go b/internal/handlers/handler_oidc_jwks.go
new file mode 100644
index 00000000..6ca68f95
--- /dev/null
+++ b/internal/handlers/handler_oidc_jwks.go
@@ -0,0 +1,15 @@
+package handlers
+
+import (
+ "encoding/json"
+
+ "github.com/authelia/authelia/internal/middlewares"
+)
+
+func oidcJWKs(ctx *middlewares.AutheliaCtx) {
+ ctx.SetContentType("application/json")
+
+ if err := json.NewEncoder(ctx).Encode(ctx.Providers.OpenIDConnect.GetKeySet()); err != nil {
+ ctx.Error(err, "failed to serve jwk set")
+ }
+}
diff --git a/internal/handlers/handler_oidc_revoke.go b/internal/handlers/handler_oidc_revoke.go
new file mode 100644
index 00000000..ac6f9319
--- /dev/null
+++ b/internal/handlers/handler_oidc_revoke.go
@@ -0,0 +1,13 @@
+package handlers
+
+import (
+ "net/http"
+
+ "github.com/authelia/authelia/internal/middlewares"
+)
+
+func oidcRevoke(ctx *middlewares.AutheliaCtx, rw http.ResponseWriter, req *http.Request) {
+ err := ctx.Providers.OpenIDConnect.Fosite.NewRevocationRequest(ctx, req)
+
+ ctx.Providers.OpenIDConnect.Fosite.WriteRevocationResponse(rw, err)
+}
diff --git a/internal/handlers/handler_oidc_token.go b/internal/handlers/handler_oidc_token.go
new file mode 100644
index 00000000..ce7f962c
--- /dev/null
+++ b/internal/handlers/handler_oidc_token.go
@@ -0,0 +1,46 @@
+package handlers
+
+import (
+ "net/http"
+
+ "github.com/ory/fosite"
+
+ "github.com/authelia/authelia/internal/middlewares"
+)
+
+func oidcToken(ctx *middlewares.AutheliaCtx, rw http.ResponseWriter, req *http.Request) {
+ oidcSession, err := newDefaultOIDCSession(ctx)
+ if err != nil {
+ ctx.Logger.Errorf("Error occurred in NewDefaultOIDCSession: %+v", err)
+ ctx.Providers.OpenIDConnect.Fosite.WriteAccessError(rw, nil, err)
+
+ return
+ }
+
+ accessRequest, accessReqErr := ctx.Providers.OpenIDConnect.Fosite.NewAccessRequest(ctx, req, oidcSession)
+ if accessReqErr != nil {
+ ctx.Logger.Errorf("Error occurred in NewAccessRequest: %+v", accessRequest)
+ ctx.Providers.OpenIDConnect.Fosite.WriteAccessError(rw, accessRequest, accessReqErr)
+
+ return
+ }
+
+ // If this is a client_credentials grant, grant all scopes the client is allowed to perform.
+ if accessRequest.GetGrantTypes().ExactOne("client_credentials") {
+ for _, scope := range accessRequest.GetRequestedScopes() {
+ if fosite.HierarchicScopeStrategy(accessRequest.GetClient().GetScopes(), scope) {
+ accessRequest.GrantScope(scope)
+ }
+ }
+ }
+
+ response, err := ctx.Providers.OpenIDConnect.Fosite.NewAccessResponse(ctx, accessRequest)
+ if err != nil {
+ ctx.Logger.Errorf("Error occurred in NewAccessResponse: %+v", err)
+ ctx.Providers.OpenIDConnect.Fosite.WriteAccessError(rw, accessRequest, err)
+
+ return
+ }
+
+ ctx.Providers.OpenIDConnect.Fosite.WriteAccessResponse(rw, accessRequest, response)
+}
diff --git a/internal/handlers/handler_oidc_wellknown.go b/internal/handlers/handler_oidc_wellknown.go
new file mode 100644
index 00000000..ca0fa06a
--- /dev/null
+++ b/internal/handlers/handler_oidc_wellknown.go
@@ -0,0 +1,73 @@
+package handlers
+
+import (
+ "encoding/json"
+ "fmt"
+
+ "github.com/valyala/fasthttp"
+
+ "github.com/authelia/authelia/internal/middlewares"
+)
+
+func oidcWellKnown(ctx *middlewares.AutheliaCtx) {
+ var configuration WellKnownConfigurationJSON
+
+ issuer, err := ctx.ForwardedProtoHost()
+ if err != nil {
+ ctx.Logger.Errorf("Error occurred in ForwardedProtoHost: %+v", err)
+ ctx.Response.SetStatusCode(fasthttp.StatusBadRequest)
+
+ return
+ }
+
+ configuration.Issuer = issuer
+ configuration.AuthURL = fmt.Sprintf("%s%s", issuer, oidcAuthorizePath)
+ configuration.TokenURL = fmt.Sprintf("%s%s", issuer, oidcTokenPath)
+ configuration.RevocationEndpoint = fmt.Sprintf("%s%s", issuer, oidcRevokePath)
+ configuration.JWKSURL = fmt.Sprintf("%s%s", issuer, oidcJWKsPath)
+ configuration.Algorithms = []string{"RS256"}
+ configuration.ScopesSupported = []string{
+ "openid",
+ "profile",
+ "groups",
+ "email",
+ // Determine if this is really mandatory knowing the RP can request for a refresh token through the authorize
+ // endpoint anyway.
+ "offline_access",
+ }
+ configuration.ClaimsSupported = []string{
+ "aud",
+ "exp",
+ "iat",
+ "iss",
+ "jti",
+ "rat",
+ "sub",
+ "auth_time",
+ "nonce",
+ "email",
+ "email_verified",
+ "groups",
+ "name",
+ }
+ configuration.ResponseTypesSupported = []string{
+ "code",
+ "token",
+ "id_token",
+ "code token",
+ "code id_token",
+ "token id_token",
+ "code token id_token",
+ "none",
+ }
+
+ ctx.SetContentType("application/json")
+
+ if err := json.NewEncoder(ctx).Encode(configuration); err != nil {
+ ctx.Logger.Errorf("Error occurred in json Encode: %+v", err)
+ // TODO: Determine if this is the appropriate error code here.
+ ctx.Response.SetStatusCode(fasthttp.StatusInternalServerError)
+
+ return
+ }
+}
diff --git a/internal/handlers/handler_sign_duo.go b/internal/handlers/handler_sign_duo.go
index 2eaedb1b..bf4666e4 100644
--- a/internal/handlers/handler_sign_duo.go
+++ b/internal/handlers/handler_sign_duo.go
@@ -73,6 +73,10 @@ func SecondFactorDuoPost(duoAPI duo.API) middlewares.RequestHandler {
return
}
- Handle2FAResponse(ctx, requestBody.TargetURL)
+ if userSession.OIDCWorkflowSession != nil {
+ HandleOIDCWorkflowResponse(ctx)
+ } else {
+ Handle2FAResponse(ctx, requestBody.TargetURL)
+ }
}
}
diff --git a/internal/handlers/handler_sign_totp.go b/internal/handlers/handler_sign_totp.go
index 3425b68b..bc604e13 100644
--- a/internal/handlers/handler_sign_totp.go
+++ b/internal/handlers/handler_sign_totp.go
@@ -10,8 +10,8 @@ import (
// SecondFactorTOTPPost validate the TOTP passcode provided by the user.
func SecondFactorTOTPPost(totpVerifier TOTPVerifier) middlewares.RequestHandler {
return func(ctx *middlewares.AutheliaCtx) {
- bodyJSON := signTOTPRequestBody{}
- err := ctx.ParseBody(&bodyJSON)
+ requestBody := signTOTPRequestBody{}
+ err := ctx.ParseBody(&requestBody)
if err != nil {
handleAuthenticationUnauthorized(ctx, err, mfaValidationFailedMessage)
@@ -26,7 +26,7 @@ func SecondFactorTOTPPost(totpVerifier TOTPVerifier) middlewares.RequestHandler
return
}
- isValid, err := totpVerifier.Verify(bodyJSON.Token, secret)
+ isValid, err := totpVerifier.Verify(requestBody.Token, secret)
if err != nil {
handleAuthenticationUnauthorized(ctx, fmt.Errorf("Error occurred during OTP validation for user %s: %s", userSession.Username, err), mfaValidationFailedMessage)
return
@@ -52,6 +52,10 @@ func SecondFactorTOTPPost(totpVerifier TOTPVerifier) middlewares.RequestHandler
return
}
- Handle2FAResponse(ctx, bodyJSON.TargetURL)
+ if userSession.OIDCWorkflowSession != nil {
+ HandleOIDCWorkflowResponse(ctx)
+ } else {
+ Handle2FAResponse(ctx, requestBody.TargetURL)
+ }
}
}
diff --git a/internal/handlers/handler_sign_u2f_step2.go b/internal/handlers/handler_sign_u2f_step2.go
index 5a51722f..e7bd75f1 100644
--- a/internal/handlers/handler_sign_u2f_step2.go
+++ b/internal/handlers/handler_sign_u2f_step2.go
@@ -55,6 +55,10 @@ func SecondFactorU2FSignPost(u2fVerifier U2FVerifier) middlewares.RequestHandler
return
}
- Handle2FAResponse(ctx, requestBody.TargetURL)
+ if userSession.OIDCWorkflowSession != nil {
+ HandleOIDCWorkflowResponse(ctx)
+ } else {
+ Handle2FAResponse(ctx, requestBody.TargetURL)
+ }
}
}
diff --git a/internal/handlers/handler_verify.go b/internal/handlers/handler_verify.go
index 951c8d85..0b08059a 100644
--- a/internal/handlers/handler_verify.go
+++ b/internal/handlers/handler_verify.go
@@ -31,49 +31,6 @@ func isSchemeWSS(url *url.URL) bool {
return url.Scheme == "wss"
}
-// getOriginalURL extract the URL from the request headers (X-Original-URI or X-Forwarded-* headers).
-func getOriginalURL(ctx *middlewares.AutheliaCtx) (*url.URL, error) {
- originalURL := ctx.XOriginalURL()
- if originalURL != nil {
- url, err := url.ParseRequestURI(string(originalURL))
- if err != nil {
- return nil, fmt.Errorf("Unable to parse URL extracted from X-Original-URL header: %v", err)
- }
-
- ctx.Logger.Trace("Using X-Original-URL header content as targeted site URL")
-
- return url, nil
- }
-
- forwardedProto := ctx.XForwardedProto()
- forwardedHost := ctx.XForwardedHost()
- forwardedURI := ctx.XForwardedURI()
-
- if forwardedProto == nil {
- return nil, errMissingXForwardedProto
- }
-
- if forwardedHost == nil {
- return nil, errMissingXForwardedHost
- }
-
- var requestURI string
-
- scheme := append(forwardedProto, protoHostSeparator...)
- requestURI = string(append(scheme,
- append(forwardedHost, forwardedURI...)...))
-
- url, err := url.ParseRequestURI(requestURI)
- if err != nil {
- return nil, fmt.Errorf("Unable to parse URL %s: %v", requestURI, err)
- }
-
- ctx.Logger.Tracef("Using X-Forwarded-Proto, X-Forwarded-Host and X-Forwarded-URI headers " +
- "to construct targeted site URL")
-
- return url, nil
-}
-
// parseBasicAuth parses an HTTP Basic Authentication string.
// "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==" returns ("Aladdin", "open sesame", true).
func parseBasicAuth(header, auth string) (username, password string, err error) {
@@ -468,7 +425,7 @@ func VerifyGet(cfg schema.AuthenticationBackendConfiguration) middlewares.Reques
return func(ctx *middlewares.AutheliaCtx) {
ctx.Logger.Tracef("Headers=%s", ctx.Request.Header.String())
- targetURL, err := getOriginalURL(ctx)
+ targetURL, err := ctx.GetOriginalURL()
if err != nil {
ctx.Error(fmt.Errorf("Unable to parse target URL: %s", err), operationFailedMessage)
diff --git a/internal/handlers/handler_verify_test.go b/internal/handlers/handler_verify_test.go
index b1427fea..14238805 100644
--- a/internal/handlers/handler_verify_test.go
+++ b/internal/handlers/handler_verify_test.go
@@ -26,49 +26,13 @@ var verifyGetCfg = schema.AuthenticationBackendConfiguration{
LDAP: &schema.LDAPAuthenticationBackendConfiguration{},
}
-// Test getOriginalURL.
-func TestShouldGetOriginalURLFromOriginalURLHeader(t *testing.T) {
- mock := mocks.NewMockAutheliaCtx(t)
- defer mock.Close()
-
- mock.Ctx.Request.Header.Set("X-Original-URL", "https://home.example.com")
- originalURL, err := getOriginalURL(mock.Ctx)
- assert.NoError(t, err)
-
- expectedURL, err := url.ParseRequestURI("https://home.example.com")
- assert.NoError(t, err)
- assert.Equal(t, expectedURL, originalURL)
-}
-
-func TestShouldGetOriginalURLFromForwardedHeadersWithoutURI(t *testing.T) {
- mock := mocks.NewMockAutheliaCtx(t)
- defer mock.Close()
- mock.Ctx.Request.Header.Set("X-Forwarded-Proto", "https")
- mock.Ctx.Request.Header.Set("X-Forwarded-Host", "home.example.com")
- originalURL, err := getOriginalURL(mock.Ctx)
- assert.NoError(t, err)
-
- expectedURL, err := url.ParseRequestURI("https://home.example.com")
- assert.NoError(t, err)
- assert.Equal(t, expectedURL, originalURL)
-}
-
-func TestShouldGetOriginalURLFromForwardedHeadersWithURI(t *testing.T) {
- mock := mocks.NewMockAutheliaCtx(t)
- defer mock.Close()
- mock.Ctx.Request.Header.Set("X-Original-URL", "htt-ps//home?-.example.com")
- _, err := getOriginalURL(mock.Ctx)
- assert.Error(t, err)
- assert.Equal(t, "Unable to parse URL extracted from X-Original-URL header: parse \"htt-ps//home?-.example.com\": invalid URI for request", err.Error())
-}
-
func TestShouldRaiseWhenTargetUrlIsMalformed(t *testing.T) {
mock := mocks.NewMockAutheliaCtx(t)
defer mock.Close()
mock.Ctx.Request.Header.Set("X-Forwarded-Proto", "https")
mock.Ctx.Request.Header.Set("X-Forwarded-Host", "home.example.com")
mock.Ctx.Request.Header.Set("X-Forwarded-URI", "/abc")
- originalURL, err := getOriginalURL(mock.Ctx)
+ originalURL, err := mock.Ctx.GetOriginalURL()
assert.NoError(t, err)
expectedURL, err := url.ParseRequestURI("https://home.example.com/abc")
@@ -79,7 +43,7 @@ func TestShouldRaiseWhenTargetUrlIsMalformed(t *testing.T) {
func TestShouldRaiseWhenNoHeaderProvidedToDetectTargetURL(t *testing.T) {
mock := mocks.NewMockAutheliaCtx(t)
defer mock.Close()
- _, err := getOriginalURL(mock.Ctx)
+ _, err := mock.Ctx.GetOriginalURL()
assert.Error(t, err)
assert.Equal(t, "Missing header X-Forwarded-Proto", err.Error())
}
@@ -89,7 +53,7 @@ func TestShouldRaiseWhenNoXForwardedHostHeaderProvidedToDetectTargetURL(t *testi
defer mock.Close()
mock.Ctx.Request.Header.Set("X-Forwarded-Proto", "https")
- _, err := getOriginalURL(mock.Ctx)
+ _, err := mock.Ctx.GetOriginalURL()
assert.Error(t, err)
assert.Equal(t, "Missing header X-Forwarded-Host", err.Error())
}
@@ -101,7 +65,7 @@ func TestShouldRaiseWhenXForwardedProtoIsNotParsable(t *testing.T) {
mock.Ctx.Request.Header.Set("X-Forwarded-Proto", "!:;;:,")
mock.Ctx.Request.Header.Set("X-Forwarded-Host", "myhost.local")
- _, err := getOriginalURL(mock.Ctx)
+ _, err := mock.Ctx.GetOriginalURL()
assert.Error(t, err)
assert.Equal(t, "Unable to parse URL !:;;:,://myhost.local: parse \"!:;;:,://myhost.local\": invalid URI for request", err.Error())
}
@@ -114,7 +78,7 @@ func TestShouldRaiseWhenXForwardedURIIsNotParsable(t *testing.T) {
mock.Ctx.Request.Header.Set("X-Forwarded-Host", "myhost.local")
mock.Ctx.Request.Header.Set("X-Forwarded-URI", "!:;;:,")
- _, err := getOriginalURL(mock.Ctx)
+ _, err := mock.Ctx.GetOriginalURL()
require.Error(t, err)
assert.Equal(t, "Unable to parse URL https://myhost.local!:;;:,: parse \"https://myhost.local!:;;:,\": invalid port \":,\" after host", err.Error())
}
diff --git a/internal/handlers/oidc.go b/internal/handlers/oidc.go
new file mode 100644
index 00000000..7c650939
--- /dev/null
+++ b/internal/handlers/oidc.go
@@ -0,0 +1,106 @@
+package handlers
+
+import (
+ "time"
+
+ "github.com/ory/fosite"
+ "github.com/ory/fosite/handler/openid"
+ "github.com/ory/fosite/token/jwt"
+
+ "github.com/authelia/authelia/internal/middlewares"
+ "github.com/authelia/authelia/internal/session"
+ "github.com/authelia/authelia/internal/utils"
+)
+
+// isConsentMissing compares the requestedScopes and requestedAudience to the workflows
+// GrantedScopes and GrantedAudience and returns true if they do not match or the workflow is nil.
+func isConsentMissing(workflow *session.OIDCWorkflowSession, requestedScopes, requestedAudience []string) (isMissing bool) {
+ if workflow == nil {
+ return true
+ }
+
+ return len(requestedScopes) > 0 && utils.IsStringSlicesDifferent(requestedScopes, workflow.GrantedScopes) ||
+ len(requestedAudience) > 0 && utils.IsStringSlicesDifferentFold(requestedAudience, workflow.GrantedAudience)
+}
+
+func scopeNamesToScopes(scopeSlice []string) (scopes []Scope) {
+ for _, name := range scopeSlice {
+ if val, ok := scopeDescriptions[name]; ok {
+ scopes = append(scopes, Scope{name, val})
+ } else {
+ scopes = append(scopes, Scope{name, name})
+ }
+ }
+
+ return scopes
+}
+
+func audienceNamesToAudience(scopeSlice []string) (audience []Audience) {
+ for _, name := range scopeSlice {
+ if val, ok := audienceDescriptions[name]; ok {
+ audience = append(audience, Audience{name, val})
+ } else {
+ audience = append(audience, Audience{name, name})
+ }
+ }
+
+ return audience
+}
+
+func newOIDCSession(ctx *middlewares.AutheliaCtx, ar fosite.AuthorizeRequester) (session *openid.DefaultSession, err error) {
+ userSession := ctx.GetSession()
+
+ scopes := ar.GetGrantedScopes()
+
+ extra := map[string]interface{}{}
+
+ if len(userSession.Emails) != 0 && scopes.Has("email") {
+ extra["email"] = userSession.Emails[0]
+ extra["email_verified"] = true
+ }
+
+ if scopes.Has("groups") {
+ extra["groups"] = userSession.Groups
+ }
+
+ if scopes.Has("profile") {
+ extra["name"] = userSession.DisplayName
+ }
+
+ /*
+ TODO: Adjust auth backends to return more profile information.
+ It's probably ideal to adjust the auth providers at this time to not store 'extra' information in the session
+ storage, and instead create a memory only storage for them.
+ This is a simple design, have a map with a key of username, and a struct with the relevant information.
+ */
+
+ oidcSession, err := newDefaultOIDCSession(ctx)
+ if oidcSession == nil {
+ return nil, err
+ }
+
+ oidcSession.Claims.Extra = extra
+ oidcSession.Claims.Subject = userSession.Username
+ oidcSession.Claims.Audience = ar.GetGrantedAudience()
+
+ return oidcSession, err
+}
+
+func newDefaultOIDCSession(ctx *middlewares.AutheliaCtx) (session *openid.DefaultSession, err error) {
+ issuer, err := ctx.ForwardedProtoHost()
+
+ return &openid.DefaultSession{
+ Claims: &jwt.IDTokenClaims{
+ Issuer: issuer,
+ // TODO(c.michaud): make this configurable
+ ExpiresAt: time.Now().Add(time.Hour * 6),
+ IssuedAt: time.Now(),
+ RequestedAt: time.Now(),
+ AuthTime: time.Now(),
+ Extra: make(map[string]interface{}),
+ },
+ Headers: &jwt.Headers{
+ Extra: make(map[string]interface{}),
+ },
+ }, err
+}
diff --git a/internal/handlers/oidc_test.go b/internal/handlers/oidc_test.go
new file mode 100644
index 00000000..7e77e6a3
--- /dev/null
+++ b/internal/handlers/oidc_test.go
@@ -0,0 +1,33 @@
+package handlers
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+
+ "github.com/authelia/authelia/internal/session"
+)
+
+func TestShouldDetectIfConsentIsMissing(t *testing.T) {
+ var workflow *session.OIDCWorkflowSession
+
+ requestedScopes := []string{"openid", "profile"}
+ requestedAudience := []string{"https://authelia.com"}
+
+ assert.True(t, isConsentMissing(workflow, requestedScopes, requestedAudience))
+
+ workflow = &session.OIDCWorkflowSession{
+ GrantedScopes: []string{"openid", "profile"},
+ GrantedAudience: []string{"https://authelia.com"},
+ }
+
+ assert.False(t, isConsentMissing(workflow, requestedScopes, requestedAudience))
+
+ requestedScopes = []string{"openid", "profile", "group"}
+
+ assert.True(t, isConsentMissing(workflow, requestedScopes, requestedAudience))
+
+ requestedScopes = []string{"openid", "profile"}
+ requestedAudience = []string{"https://not.authelia.com"}
+ assert.True(t, isConsentMissing(workflow, requestedScopes, requestedAudience))
+}
diff --git a/internal/handlers/register_oidc.go b/internal/handlers/register_oidc.go
new file mode 100644
index 00000000..75d34fab
--- /dev/null
+++ b/internal/handlers/register_oidc.go
@@ -0,0 +1,29 @@
+package handlers
+
+import (
+ "github.com/fasthttp/router"
+
+ "github.com/authelia/authelia/internal/middlewares"
+)
+
+// RegisterOIDC registers the handlers with the fasthttp *router.Router. TODO: Add paths for UserInfo, Flush, Logout.
+func RegisterOIDC(router *router.Router, middleware middlewares.RequestHandlerBridge) {
+ // TODO: Add OPTIONS handler.
+ router.GET(oidcWellKnownPath, middleware(oidcWellKnown))
+
+ router.GET(oidcConsentPath, middleware(oidcConsent))
+
+ router.POST(oidcConsentPath, middleware(oidcConsentPOST))
+
+ router.GET(oidcJWKsPath, middleware(oidcJWKs))
+
+ router.GET(oidcAuthorizePath, middleware(middlewares.NewHTTPToAutheliaHandlerAdaptor(oidcAuthorize)))
+
+ // TODO: Add OPTIONS handler.
+ router.POST(oidcTokenPath, middleware(middlewares.NewHTTPToAutheliaHandlerAdaptor(oidcToken)))
+
+ router.POST(oidcIntrospectPath, middleware(middlewares.NewHTTPToAutheliaHandlerAdaptor(oidcIntrospect)))
+
+ // TODO: Add OPTIONS handler.
+ router.POST(oidcRevokePath, middleware(middlewares.NewHTTPToAutheliaHandlerAdaptor(oidcRevoke)))
+}
diff --git a/internal/handlers/response.go b/internal/handlers/response.go
index ebff9dbb..70220879 100644
--- a/internal/handlers/response.go
+++ b/internal/handlers/response.go
@@ -11,6 +11,42 @@ import (
"github.com/authelia/authelia/internal/utils"
)
+// HandleOIDCWorkflowResponse handle the redirection upon authentication in the OIDC workflow.
+func HandleOIDCWorkflowResponse(ctx *middlewares.AutheliaCtx) {
+ userSession := ctx.GetSession()
+
+ if !authorization.IsAuthLevelSufficient(userSession.AuthenticationLevel, userSession.OIDCWorkflowSession.RequiredAuthorizationLevel) {
+ ctx.Logger.Warn("OIDC requires 2FA, cannot be redirected yet")
+ ctx.ReplyOK()
+
+ return
+ }
+
+ uri, err := ctx.ForwardedProtoHost()
+ if err != nil {
+ ctx.Logger.Errorf("%v", err)
+ handleAuthenticationUnauthorized(ctx, fmt.Errorf("Unable to get forward facing URI"), authenticationFailedMessage)
+
+ return
+ }
+
+ if isConsentMissing(
+ userSession.OIDCWorkflowSession,
+ userSession.OIDCWorkflowSession.RequestedScopes,
+ userSession.OIDCWorkflowSession.RequestedAudience) {
+ err := ctx.SetJSONBody(redirectResponse{Redirect: fmt.Sprintf("%s/consent", uri)})
+
+ if err != nil {
+ ctx.Logger.Errorf("Unable to set default redirection URL in body: %s", err)
+ }
+ } else {
+ err := ctx.SetJSONBody(redirectResponse{Redirect: userSession.OIDCWorkflowSession.AuthURI})
+ if err != nil {
+ ctx.Logger.Errorf("Unable to set default redirection URL in body: %s", err)
+ }
+ }
+}
+
// Handle1FAResponse handle the redirection upon 1FA authentication.
func Handle1FAResponse(ctx *middlewares.AutheliaCtx, targetURI, requestMethod string, username string, groups []string) {
if targetURI == "" {
diff --git a/internal/handlers/types_oidc.go b/internal/handlers/types_oidc.go
new file mode 100644
index 00000000..47bf5361
--- /dev/null
+++ b/internal/handlers/types_oidc.go
@@ -0,0 +1,62 @@
+package handlers
+
+import (
+ "github.com/dgrijalva/jwt-go"
+)
+
+// ConsentPostRequestBody schema of the request body of the consent POST endpoint.
+type ConsentPostRequestBody struct {
+ ClientID string `json:"client_id"`
+ AcceptOrReject string `json:"accept_or_reject"`
+}
+
+// ConsentPostResponseBody schema of the response body of the consent POST endpoint.
+type ConsentPostResponseBody struct {
+ RedirectURI string `json:"redirect_uri"`
+}
+
+// ConsentGetResponseBody schema of the response body of the consent GET endpoint.
+type ConsentGetResponseBody struct {
+ ClientID string `json:"client_id"`
+ ClientDescription string `json:"client_description"`
+ Scopes []Scope `json:"scopes"`
+ Audience []Audience `json:"audience"`
+}
+
+// Scope represents the scope information.
+type Scope struct {
+ Name string `json:"name"`
+ Description string `json:"description"`
+}
+
+// Audience represents the audience information.
+type Audience struct {
+ Name string `json:"name"`
+ Description string `json:"description"`
+}
+
+// OIDCClaims represents a set of OIDC claims.
+type OIDCClaims struct {
+ jwt.StandardClaims
+
+ Workflow string `json:"workflow"`
+ Username string `json:"username,omitempty"`
+ RequestedScopes []string `json:"requested_scopes,omitempty"`
+}
+
+// WellKnownConfigurationJSON is the OIDC well known config struct.
+type WellKnownConfigurationJSON struct {
+ Issuer string `json:"issuer"`
+ AuthURL string `json:"authorization_endpoint"`
+ TokenURL string `json:"token_endpoint"`
+ RevocationEndpoint string `json:"revocation_endpoint"`
+ JWKSURL string `json:"jwks_uri"`
+ Algorithms []string `json:"id_token_signing_alg_values_supported"`
+ ResponseTypesSupported []string `json:"response_types_supported"`
+ ScopesSupported []string `json:"scopes_supported"`
+ ClaimsSupported []string `json:"claims_supported"`
+ BackChannelLogoutSupported bool `json:"backchannel_logout_supported"`
+ BackChannelLogoutSessionSupported bool `json:"backchannel_logout_session_supported"`
+ FrontChannelLogoutSupported bool `json:"frontchannel_logout_supported"`
+ FrontChannelLogoutSessionSupported bool `json:"frontchannel_logout_session_supported"`
+}
diff --git a/internal/middlewares/authelia_context.go b/internal/middlewares/authelia_context.go
index 1ba162e4..fe77f08a 100644
--- a/internal/middlewares/authelia_context.go
+++ b/internal/middlewares/authelia_context.go
@@ -4,6 +4,7 @@ import (
"encoding/json"
"fmt"
"net"
+ "net/url"
"strings"
"github.com/asaskevich/govalidator"
@@ -37,7 +38,7 @@ func NewAutheliaCtx(ctx *fasthttp.RequestCtx, configuration schema.Configuration
}
// AutheliaMiddleware is wrapping the RequestCtx into an AutheliaCtx providing Authelia related objects.
-func AutheliaMiddleware(configuration schema.Configuration, providers Providers) func(next RequestHandler) fasthttp.RequestHandler {
+func AutheliaMiddleware(configuration schema.Configuration, providers Providers) RequestHandlerBridge {
return func(next RequestHandler) fasthttp.RequestHandler {
return func(ctx *fasthttp.RequestCtx) {
autheliaCtx, err := NewAutheliaCtx(ctx, configuration, providers)
@@ -87,6 +88,11 @@ func (c *AutheliaCtx) ReplyForbidden() {
c.RequestCtx.Error(fasthttp.StatusMessage(fasthttp.StatusForbidden), fasthttp.StatusForbidden)
}
+// ReplyBadRequest response sent when bad request has been sent.
+func (c *AutheliaCtx) ReplyBadRequest() {
+ c.RequestCtx.Error(fasthttp.StatusMessage(fasthttp.StatusBadRequest), fasthttp.StatusBadRequest)
+}
+
// XForwardedProto return the content of the X-Forwarded-Proto header.
func (c *AutheliaCtx) XForwardedProto() []byte {
return c.RequestCtx.Request.Header.Peek(xForwardedProtoHeader)
@@ -107,6 +113,24 @@ func (c *AutheliaCtx) XForwardedURI() []byte {
return c.RequestCtx.Request.Header.Peek(xForwardedURIHeader)
}
+// ForwardedProtoHost gets the X-Forwarded-Proto and X-Forwarded-Host headers and forms them into a URL.
+func (c AutheliaCtx) ForwardedProtoHost() (string, error) {
+ XForwardedProto := c.XForwardedProto()
+
+ if XForwardedProto == nil {
+ return "", errMissingXForwardedProto
+ }
+
+ XForwardedHost := c.XForwardedHost()
+
+ if XForwardedHost == nil {
+ return "", errMissingXForwardedHost
+ }
+
+ return fmt.Sprintf("%s://%s", XForwardedProto,
+ XForwardedHost), nil
+}
+
// XOriginalURL return the content of the X-Original-URL header.
func (c *AutheliaCtx) XOriginalURL() []byte {
return c.RequestCtx.Request.Header.Peek(xOriginalURLHeader)
@@ -181,3 +205,46 @@ func (c *AutheliaCtx) RemoteIP() net.IP {
return c.RequestCtx.RemoteIP()
}
+
+// GetOriginalURL extract the URL from the request headers (X-Original-URI or X-Forwarded-* headers).
+func (c *AutheliaCtx) GetOriginalURL() (*url.URL, error) {
+ originalURL := c.XOriginalURL()
+ if originalURL != nil {
+ parsedURL, err := url.ParseRequestURI(string(originalURL))
+ if err != nil {
+ return nil, fmt.Errorf("Unable to parse URL extracted from X-Original-URL header: %v", err)
+ }
+
+ c.Logger.Trace("Using X-Original-URL header content as targeted site URL")
+
+ return parsedURL, nil
+ }
+
+ forwardedProto := c.XForwardedProto()
+ forwardedHost := c.XForwardedHost()
+ forwardedURI := c.XForwardedURI()
+
+ if forwardedProto == nil {
+ return nil, errMissingXForwardedProto
+ }
+
+ if forwardedHost == nil {
+ return nil, errMissingXForwardedHost
+ }
+
+ var requestURI string
+
+ scheme := append(forwardedProto, protoHostSeparator...)
+ requestURI = string(append(scheme,
+ append(forwardedHost, forwardedURI...)...))
+
+ parsedURL, err := url.ParseRequestURI(requestURI)
+ if err != nil {
+ return nil, fmt.Errorf("Unable to parse URL %s: %v", requestURI, err)
+ }
+
+ c.Logger.Tracef("Using X-Fowarded-Proto, X-Forwarded-Host and X-Forwarded-URI headers " +
+ "to construct targeted site URL")
+
+ return parsedURL, nil
+}
diff --git a/internal/middlewares/authelia_context_test.go b/internal/middlewares/authelia_context_test.go
index 9d6ee073..e73e1cc8 100644
--- a/internal/middlewares/authelia_context_test.go
+++ b/internal/middlewares/authelia_context_test.go
@@ -1,6 +1,7 @@
package middlewares_test
import (
+ "net/url"
"testing"
"github.com/golang/mock/gomock"
@@ -33,3 +34,39 @@ func TestShouldCallNextWithAutheliaCtx(t *testing.T) {
assert.True(t, nextCalled)
}
+
+// Test getOriginalURL.
+func TestShouldGetOriginalURLFromOriginalURLHeader(t *testing.T) {
+ mock := mocks.NewMockAutheliaCtx(t)
+ defer mock.Close()
+
+ mock.Ctx.Request.Header.Set("X-Original-URL", "https://home.example.com")
+ originalURL, err := mock.Ctx.GetOriginalURL()
+ assert.NoError(t, err)
+
+ expectedURL, err := url.ParseRequestURI("https://home.example.com")
+ assert.NoError(t, err)
+ assert.Equal(t, expectedURL, originalURL)
+}
+
+func TestShouldGetOriginalURLFromForwardedHeadersWithoutURI(t *testing.T) {
+ mock := mocks.NewMockAutheliaCtx(t)
+ defer mock.Close()
+ mock.Ctx.Request.Header.Set("X-Forwarded-Proto", "https")
+ mock.Ctx.Request.Header.Set("X-Forwarded-Host", "home.example.com")
+ originalURL, err := mock.Ctx.GetOriginalURL()
+ assert.NoError(t, err)
+
+ expectedURL, err := url.ParseRequestURI("https://home.example.com")
+ assert.NoError(t, err)
+ assert.Equal(t, expectedURL, originalURL)
+}
+
+func TestShouldGetOriginalURLFromForwardedHeadersWithURI(t *testing.T) {
+ mock := mocks.NewMockAutheliaCtx(t)
+ defer mock.Close()
+ mock.Ctx.Request.Header.Set("X-Original-URL", "htt-ps//home?-.example.com")
+ _, err := mock.Ctx.GetOriginalURL()
+ assert.Error(t, err)
+ assert.Equal(t, "Unable to parse URL extracted from X-Original-URL header: parse \"htt-ps//home?-.example.com\": invalid URI for request", err.Error())
+}
diff --git a/internal/middlewares/const.go b/internal/middlewares/const.go
index e15cee80..858c9b70 100644
--- a/internal/middlewares/const.go
+++ b/internal/middlewares/const.go
@@ -16,3 +16,5 @@ var okMessageBytes = []byte("{\"status\":\"OK\"}")
const operationFailedMessage = "Operation failed"
const identityVerificationTokenAlreadyUsedMessage = "The identity verification token has already been used"
const identityVerificationTokenHasExpiredMessage = "The identity verification token has expired"
+
+var protoHostSeparator = []byte("://")
diff --git a/internal/middlewares/http_to_authelia_handler_adaptor.go b/internal/middlewares/http_to_authelia_handler_adaptor.go
new file mode 100644
index 00000000..281e75a4
--- /dev/null
+++ b/internal/middlewares/http_to_authelia_handler_adaptor.go
@@ -0,0 +1,125 @@
+package middlewares
+
+import (
+ "io"
+ "net/http"
+ "net/url"
+
+ "github.com/valyala/fasthttp"
+)
+
+// AutheliaHandlerFunc is used with the NewHTTPToAutheliaHandlerAdaptor to encapsulate a func.
+type AutheliaHandlerFunc func(ctx *AutheliaCtx, rw http.ResponseWriter, r *http.Request)
+
+type netHTTPBody struct {
+ b []byte
+}
+
+// Read reads the body.
+func (r *netHTTPBody) Read(p []byte) (int, error) {
+ if len(r.b) == 0 {
+ return 0, io.EOF
+ }
+
+ n := copy(p, r.b)
+ r.b = r.b[n:]
+
+ return n, nil
+}
+
+// Close closes the body.
+func (r *netHTTPBody) Close() error {
+ r.b = r.b[:0]
+ return nil
+}
+
+type netHTTPResponseWriter struct {
+ statusCode int
+ h http.Header
+ body []byte
+}
+
+// StatusCode returns the status code.
+func (w *netHTTPResponseWriter) StatusCode() int {
+ if w.statusCode == 0 {
+ return http.StatusOK
+ }
+
+ return w.statusCode
+}
+
+// Header returns the http.Header.
+func (w *netHTTPResponseWriter) Header() http.Header {
+ if w.h == nil {
+ w.h = make(http.Header)
+ }
+
+ return w.h
+}
+
+// WriteHeader needs to be documented TODO: document it.
+func (w *netHTTPResponseWriter) WriteHeader(statusCode int) {
+ w.statusCode = statusCode
+}
+
+// Write writes to the body.
+func (w *netHTTPResponseWriter) Write(p []byte) (int, error) {
+ w.body = append(w.body, p...)
+ return len(p), nil
+}
+
+// NewHTTPToAutheliaHandlerAdaptor creates a new adaptor given the AutheliaHandlerFunc.
+func NewHTTPToAutheliaHandlerAdaptor(h AutheliaHandlerFunc) RequestHandler {
+ return func(ctx *AutheliaCtx) {
+ var r http.Request
+
+ body := ctx.PostBody()
+ r.Method = string(ctx.Method())
+ r.Proto = "HTTP/1.1"
+ r.ProtoMajor = 1
+ r.ProtoMinor = 1
+ r.RequestURI = string(ctx.RequestURI())
+ r.ContentLength = int64(len(body))
+ r.Host = string(ctx.Host())
+ r.RemoteAddr = ctx.RemoteAddr().String()
+
+ hdr := make(http.Header)
+ ctx.Request.Header.VisitAll(func(k, v []byte) {
+ sk := string(k)
+ sv := string(v)
+ switch sk {
+ case "Transfer-Encoding":
+ r.TransferEncoding = append(r.TransferEncoding, sv)
+ default:
+ hdr.Set(sk, sv)
+ }
+ })
+
+ r.Header = hdr
+ r.Body = &netHTTPBody{body}
+ rURL, err := url.ParseRequestURI(r.RequestURI)
+
+ if err != nil {
+ ctx.Logger.Errorf("cannot parse requestURI %q: %s", r.RequestURI, err)
+ ctx.RequestCtx.Error("Internal Server Error", fasthttp.StatusInternalServerError)
+
+ return
+ }
+
+ r.URL = rURL
+
+ var w netHTTPResponseWriter
+
+ h(ctx, &w, r.WithContext(ctx))
+
+ ctx.SetStatusCode(w.StatusCode())
+
+ for k, vv := range w.Header() {
+ for _, v := range vv {
+ ctx.Response.Header.Set(k, v)
+ }
+ }
+
+ ctx.Write(w.body) //nolint:errcheck
+ }
+}
diff --git a/internal/middlewares/identity_verification.go b/internal/middlewares/identity_verification.go
index 7b967679..aeb6ec75 100644
--- a/internal/middlewares/identity_verification.go
+++ b/internal/middlewares/identity_verification.go
@@ -6,7 +6,7 @@ import (
"fmt"
"time"
- jwt "github.com/dgrijalva/jwt-go"
+ "github.com/dgrijalva/jwt-go"
"github.com/authelia/authelia/internal/templates"
)
@@ -51,18 +51,13 @@ func IdentityVerificationStart(args IdentityVerificationStartArgs) RequestHandle
return
}
- if ctx.XForwardedProto() == nil {
- ctx.Error(errMissingXForwardedProto, operationFailedMessage)
+ uri, err := ctx.ForwardedProtoHost()
+ if err != nil {
+ ctx.Error(err, operationFailedMessage)
return
}
- if ctx.XForwardedHost() == nil {
- ctx.Error(errMissingXForwardedHost, operationFailedMessage)
- return
- }
-
- link := fmt.Sprintf("%s://%s%s%s?token=%s", ctx.XForwardedProto(),
- ctx.XForwardedHost(), ctx.Configuration.Server.Path, args.TargetEndpoint, ss)
+ link := fmt.Sprintf("%s%s%s?token=%s", uri, ctx.Configuration.Server.Path, args.TargetEndpoint, ss)
bufHTML := new(bytes.Buffer)
diff --git a/internal/middlewares/types.go b/internal/middlewares/types.go
index 753af13a..9f956d59 100644
--- a/internal/middlewares/types.go
+++ b/internal/middlewares/types.go
@@ -1,7 +1,7 @@
package middlewares
import (
- jwt "github.com/dgrijalva/jwt-go"
+ "github.com/dgrijalva/jwt-go"
"github.com/sirupsen/logrus"
"github.com/valyala/fasthttp"
@@ -9,6 +9,7 @@ import (
"github.com/authelia/authelia/internal/authorization"
"github.com/authelia/authelia/internal/configuration/schema"
"github.com/authelia/authelia/internal/notification"
+ "github.com/authelia/authelia/internal/oidc"
"github.com/authelia/authelia/internal/regulation"
"github.com/authelia/authelia/internal/session"
"github.com/authelia/authelia/internal/storage"
@@ -31,6 +32,7 @@ type Providers struct {
Authorizer *authorization.Authorizer
SessionProvider *session.Provider
Regulator *regulation.Regulator
+ OpenIDConnect oidc.OpenIDConnectProvider
UserProvider authentication.UserProvider
StorageProvider storage.Provider
@@ -43,6 +45,9 @@ type RequestHandler = func(*AutheliaCtx)
// Middleware represent an Authelia middleware.
type Middleware = func(RequestHandler) RequestHandler
+// RequestHandlerBridge bridge a AutheliaCtx handle to a RequestHandler handler.
+type RequestHandlerBridge = func(RequestHandler) fasthttp.RequestHandler
+
// IdentityVerificationStartArgs represent the arguments used to customize the starting phase
// of the identity verification process.
type IdentityVerificationStartArgs struct {
diff --git a/internal/oidc/client.go b/internal/oidc/client.go
new file mode 100644
index 00000000..d49c20d2
--- /dev/null
+++ b/internal/oidc/client.go
@@ -0,0 +1,75 @@
+package oidc
+
+import (
+ "github.com/ory/fosite"
+
+ "github.com/authelia/authelia/internal/authentication"
+ "github.com/authelia/authelia/internal/authorization"
+)
+
+// InternalClient represents the client internally.
+type InternalClient struct {
+ ID string `json:"id"`
+ Description string `json:"-"`
+ Secret []byte `json:"client_secret,omitempty"`
+ RedirectURIs []string `json:"redirect_uris"`
+ GrantTypes []string `json:"grant_types"`
+ ResponseTypes []string `json:"response_types"`
+ Scopes []string `json:"scopes"`
+ Audience []string `json:"audience"`
+ Public bool `json:"public"`
+ Policy authorization.Level `json:"-"`
+}
+
+// IsAuthenticationLevelSufficient returns if the provided authentication.Level is sufficient for the client of the AutheliaClient.
+func (c InternalClient) IsAuthenticationLevelSufficient(level authentication.Level) bool {
+ return authorization.IsAuthLevelSufficient(level, c.Policy)
+}
+
+// GetID returns the ID.
+func (c InternalClient) GetID() string {
+ return c.ID
+}
+
+// GetHashedSecret returns the Secret.
+func (c InternalClient) GetHashedSecret() []byte {
+ return c.Secret
+}
+
+// GetRedirectURIs returns the RedirectURIs.
+func (c InternalClient) GetRedirectURIs() []string {
+ return c.RedirectURIs
+}
+
+// GetGrantTypes returns the GrantTypes.
+func (c InternalClient) GetGrantTypes() fosite.Arguments {
+ if len(c.GrantTypes) == 0 {
+ return fosite.Arguments{"authorization_code"}
+ }
+
+ return c.GrantTypes
+}
+
+// GetResponseTypes returns the ResponseTypes.
+func (c InternalClient) GetResponseTypes() fosite.Arguments {
+ if len(c.ResponseTypes) == 0 {
+ return fosite.Arguments{"code"}
+ }
+
+ return c.ResponseTypes
+}
+
+// GetScopes returns the Scopes.
+func (c InternalClient) GetScopes() fosite.Arguments {
+ return c.Scopes
+}
+
+// IsPublic returns the value of the Public property.
+func (c InternalClient) IsPublic() bool {
+ return c.Public
+}
+
+// GetAudience returns the Audience.
+func (c InternalClient) GetAudience() fosite.Arguments {
+ return c.Audience
+}
diff --git a/internal/oidc/client_test.go b/internal/oidc/client_test.go
new file mode 100644
index 00000000..2ecd75ae
--- /dev/null
+++ b/internal/oidc/client_test.go
@@ -0,0 +1,34 @@
+package oidc
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+
+ "github.com/authelia/authelia/internal/authentication"
+ "github.com/authelia/authelia/internal/authorization"
+)
+
+func TestIsAuthenticationLevelSufficient(t *testing.T) {
+ c := InternalClient{}
+
+ c.Policy = authorization.Bypass
+ assert.True(t, c.IsAuthenticationLevelSufficient(authentication.NotAuthenticated))
+ assert.True(t, c.IsAuthenticationLevelSufficient(authentication.OneFactor))
+ assert.True(t, c.IsAuthenticationLevelSufficient(authentication.TwoFactor))
+
+ c.Policy = authorization.OneFactor
+ assert.False(t, c.IsAuthenticationLevelSufficient(authentication.NotAuthenticated))
+ assert.True(t, c.IsAuthenticationLevelSufficient(authentication.OneFactor))
+ assert.True(t, c.IsAuthenticationLevelSufficient(authentication.TwoFactor))
+
+ c.Policy = authorization.TwoFactor
+ assert.False(t, c.IsAuthenticationLevelSufficient(authentication.NotAuthenticated))
+ assert.False(t, c.IsAuthenticationLevelSufficient(authentication.OneFactor))
+ assert.True(t, c.IsAuthenticationLevelSufficient(authentication.TwoFactor))
+
+ c.Policy = authorization.Denied
+ assert.False(t, c.IsAuthenticationLevelSufficient(authentication.NotAuthenticated))
+ assert.False(t, c.IsAuthenticationLevelSufficient(authentication.OneFactor))
+ assert.False(t, c.IsAuthenticationLevelSufficient(authentication.TwoFactor))
+}
diff --git a/internal/oidc/errors.go b/internal/oidc/errors.go
new file mode 100644
index 00000000..239b3e54
--- /dev/null
+++ b/internal/oidc/errors.go
@@ -0,0 +1,5 @@
+package oidc
+
+import "errors"
+
+var errPasswordsDoNotMatch = errors.New("the passwords don't match")
diff --git a/internal/oidc/hasher.go b/internal/oidc/hasher.go
new file mode 100644
index 00000000..29267080
--- /dev/null
+++ b/internal/oidc/hasher.go
@@ -0,0 +1,24 @@
+package oidc
+
+import (
+ "context"
+ "crypto/subtle"
+)
+
+// AutheliaHasher implements the fosite.Hasher interface without an actual hashing algo.
+type AutheliaHasher struct {
+}
+
+// Compare compares the hash with the data and returns an error if they don't match.
+func (h AutheliaHasher) Compare(ctx context.Context, hash, data []byte) (err error) {
+ if subtle.ConstantTimeCompare(hash, data) == 0 {
+ return errPasswordsDoNotMatch
+ }
+
+ return nil
+}
+
+// Hash creates a new hash from data.
+func (h AutheliaHasher) Hash(ctx context.Context, data []byte) (hash []byte, err error) {
+ return data, nil
+}
diff --git a/internal/oidc/hasher_test.go b/internal/oidc/hasher_test.go
new file mode 100644
index 00000000..bc93a235
--- /dev/null
+++ b/internal/oidc/hasher_test.go
@@ -0,0 +1,47 @@
+package oidc
+
+import (
+ "context"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestShouldNotRaiseErrorOnEqualPasswordsPlainText(t *testing.T) {
+ hasher := AutheliaHasher{}
+
+ a := []byte("abc")
+ b := []byte("abc")
+
+ ctx := context.Background()
+
+ err := hasher.Compare(ctx, a, b)
+
+ assert.NoError(t, err)
+}
+
+func TestShouldRaiseErrorOnNonEqualPasswordsPlainText(t *testing.T) {
+ hasher := AutheliaHasher{}
+
+ a := []byte("abc")
+ b := []byte("abcd")
+
+ ctx := context.Background()
+
+ err := hasher.Compare(ctx, a, b)
+
+ assert.Equal(t, errPasswordsDoNotMatch, err)
+}
+
+func TestShouldHashPassword(t *testing.T) {
+ hasher := AutheliaHasher{}
+
+ data := []byte("abc")
+
+ ctx := context.Background()
+
+ hash, err := hasher.Hash(ctx, data)
+
+ assert.NoError(t, err)
+ assert.Equal(t, data, hash)
+}
diff --git a/internal/oidc/provider.go b/internal/oidc/provider.go
new file mode 100644
index 00000000..cf9ab8a7
--- /dev/null
+++ b/internal/oidc/provider.go
@@ -0,0 +1,108 @@
+package oidc
+
+import (
+ "crypto/rsa"
+ "fmt"
+
+ "github.com/ory/fosite"
+ "github.com/ory/fosite/compose"
+ "github.com/ory/fosite/token/jwt"
+ "gopkg.in/square/go-jose.v2"
+
+ "github.com/authelia/authelia/internal/configuration/schema"
+ "github.com/authelia/authelia/internal/utils"
+)
+
+// OpenIDConnectProvider for OpenID Connect.
+type OpenIDConnectProvider struct {
+ privateKeys map[string]*rsa.PrivateKey
+
+ Fosite fosite.OAuth2Provider
+ Store *OpenIDConnectStore
+}
+
+// NewOpenIDConnectProvider new-ups a OpenIDConnectProvider.
+func NewOpenIDConnectProvider(configuration *schema.OpenIDConnectConfiguration) (provider OpenIDConnectProvider, err error) {
+ provider = OpenIDConnectProvider{
+ Fosite: nil,
+ }
+
+ if configuration == nil {
+ return provider, nil
+ }
+
+ provider.Store = NewOpenIDConnectStore(configuration)
+
+ composeConfiguration := new(compose.Config)
+
+ key, err := utils.ParseRsaPrivateKeyFromPemStr(configuration.IssuerPrivateKey)
+ if err != nil {
+ return provider, fmt.Errorf("unable to parse the private key of the OpenID issuer: %w", err)
+ }
+
+ provider.privateKeys = make(map[string]*rsa.PrivateKey)
+ provider.privateKeys["main-key"] = key
+
+ // TODO: Consider implementing RS512 as well.
+ jwtStrategy := &jwt.RS256JWTStrategy{PrivateKey: key}
+
+ strategy := &compose.CommonStrategy{
+ CoreStrategy: compose.NewOAuth2HMACStrategy(
+ composeConfiguration,
+ []byte(utils.HashSHA256FromString(configuration.HMACSecret)),
+ nil,
+ ),
+ OpenIDConnectTokenStrategy: compose.NewOpenIDConnectStrategy(
+ composeConfiguration,
+ provider.privateKeys["main-key"],
+ ),
+ JWTStrategy: jwtStrategy,
+ }
+
+ provider.Fosite = compose.Compose(
+ composeConfiguration,
+ provider.Store,
+ strategy,
+ AutheliaHasher{},
+
+ /*
+ These are the OAuth2 and OpenIDConnect factories. Order is important (the OAuth2 factories at the top must
+ be before the OpenIDConnect factories) and taken directly from fosite.compose.ComposeAllEnabled. The
+ commented factories are not enabled as we don't yet use them but are still here for reference purposes.
+ */
+ compose.OAuth2AuthorizeExplicitFactory,
+ compose.OAuth2AuthorizeImplicitFactory,
+ compose.OAuth2ClientCredentialsGrantFactory,
+ compose.OAuth2RefreshTokenGrantFactory,
+ compose.OAuth2ResourceOwnerPasswordCredentialsFactory,
+ // compose.RFC7523AssertionGrantFactory,
+
+ compose.OpenIDConnectExplicitFactory,
+ compose.OpenIDConnectImplicitFactory,
+ compose.OpenIDConnectHybridFactory,
+ compose.OpenIDConnectRefreshFactory,
+
+ compose.OAuth2TokenIntrospectionFactory,
+ compose.OAuth2TokenRevocationFactory,
+
+ // compose.OAuth2PKCEFactory,
+ )
+
+ return provider, nil
+}
+
+// GetKeySet returns the jose.JSONWebKeySet for the OpenIDConnectProvider.
+func (p OpenIDConnectProvider) GetKeySet() (webKeySet jose.JSONWebKeySet) {
+ for keyID, key := range p.privateKeys {
+ webKey := jose.JSONWebKey{
+ Key: &key.PublicKey,
+ KeyID: keyID,
+ Algorithm: "RS256",
+ Use: "sig",
+ }
+
+ webKeySet.Keys = append(webKeySet.Keys, webKey)
+ }
+
+ return webKeySet
+}
diff --git a/internal/oidc/provider_test.go b/internal/oidc/provider_test.go
new file mode 100644
index 00000000..9eacfad2
--- /dev/null
+++ b/internal/oidc/provider_test.go
@@ -0,0 +1,40 @@
+package oidc
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+
+ "github.com/authelia/authelia/internal/configuration/schema"
+)
+
+var exampleIssuerPrivateKey = "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEAvcMVMB2vEbqI6PlSNJ4HmUyMxBDJ5iY7FS+zDDAHOZBg9S3S\nKcAn1CZcnyL0VvJ7wcdhR6oTnOwR94eKvzUyJZ+GL2hTMm27dubEYsNdhoCl6N3X\nyEEohNfoxiiCYraVauX8X3M9jFzbEz9+pacaDbHB2syaJ1qFmMNR+HSu2jPzOo7M\nlqKIOgUzA0741MaYNt47AEVg4XU5ORLdolbAkItmYg1QbyFndg9H5IvwKkYaXTGE\nlgDBcPUC0yVjAC15Mguquq+jZeQay+6PSbHTD8PQMOkLjyChI2xEhVNbdCXe676R\ncMW2R/gjrcK23zmtmTWRfdC1iZLSlHO+bJj9vQIDAQABAoIBAEZvkP/JJOCJwqPn\nV3IcbmmilmV4bdi1vByDFgyiDyx4wOSA24+PubjvfFW9XcCgRPuKjDtTj/AhWBHv\nB7stfa2lZuNV7/u562mZArA+IAr62Zp0LdIxDV8x3T8gbjVB3HhPYbv0RJZDKTYd\nzV6jhfIrVu9mHpoY6ZnodhapCPYIyk/d49KBIHZuAc25CUjMXgTeaVtf0c996036\nUxW6ef33wAOJAvW0RCvbXAJfmBeEq2qQlkjTIlpYx71fhZWexHifi8Ouv3Zonc+1\n/P2Adq5uzYVBT92f9RKHg9QxxNzVrLjSMaxyvUtWQCAQfW0tFIRdqBGsHYsQrFtI\nF4yzv8ECgYEA7ntpyN9HD9Z9lYQzPCR73sFCLM+ID99aVij0wHuxK97bkSyyvkLd\n7MyTaym3lg1UEqWNWBCLvFULZx7F0Ah6qCzD4ymm3Bj/ADpWWPgljBI0AFml+HHs\nhcATmXUrj5QbLyhiP2gmJjajp1o/rgATx6ED66seSynD6JOH8wUhhZUCgYEAy7OA\n06PF8GfseNsTqlDjNF0K7lOqd21S0prdwrsJLiVzUlfMM25MLE0XLDUutCnRheeh\nIlcuDoBsVTxz6rkvFGD74N+pgXlN4CicsBq5ofK060PbqCQhSII3fmHobrZ9Cr75\nHmBjAxHx998SKaAAGbBbcYGUAp521i1pH5CEPYkCgYEAkUd1Zf0+2RMdZhwm6hh/\nrW+l1I6IoMK70YkZsLipccRNld7Y9LbfYwYtODcts6di9AkOVfueZJiaXbONZfIE\nZrb+jkAteh9wGL9xIrnohbABJcV3Kiaco84jInUSmGDtPokncOENfHIEuEpuSJ2b\nbx1TuhmAVuGWivR0+ULC7RECgYEAgS0cDRpWc9Xzh9Cl7+PLsXEvdWNpPsL9OsEq\n0Ep7z9+/+f/jZtoTRCS/BTHUpDvAuwHglT5j3p5iFMt5VuiIiovWLwynGYwrbnNS\nqfrIrYKUaH1n1oDS+oBZYLQGCe9/7EifAjxtjYzbvSyg//SPG7tSwfBCREbpZXj2\nqSWkNsECgYA/mCDzCTlrrWPuiepo6kTmN+4TnFA+hJI6NccDVQ+jvbqEdoJ4SW4L\nzqfZSZRFJMNpSgIqkQNRPJqMP0jQ5KRtJrjMWBnYxktwKz9fDg2R2MxdFgMF2LH2\nHEMMhFHlv8NDjVOXh1KwRoltNGVWYsSrD9wKU9GhRCEfmNCGrvBcEg==\n-----END RSA PRIVATE KEY-----"
+
+func TestOpenIDConnectProvider_NewOpenIDConnectProvider_NotConfigured(t *testing.T) {
+ provider, err := NewOpenIDConnectProvider(nil)
+
+ assert.NoError(t, err)
+ assert.Nil(t, provider.Fosite)
+ assert.Nil(t, provider.Store)
+}
+
+func TestOpenIDConnectProvider_NewOpenIDConnectProvider_BadIssuerKey(t *testing.T) {
+ _, err := NewOpenIDConnectProvider(&schema.OpenIDConnectConfiguration{
+ IssuerPrivateKey: "BAD KEY",
+ })
+
+ assert.Error(t, err, "abc")
+}
+
+func TestOpenIDConnectProvider_GetKeySet(t *testing.T) {
+ p, err := NewOpenIDConnectProvider(&schema.OpenIDConnectConfiguration{
+ IssuerPrivateKey: exampleIssuerPrivateKey,
+ })
+
+ assert.NoError(t, err)
+
+ assert.Len(t, p.GetKeySet().Keys, 1)
+ assert.Equal(t, "RS256", p.GetKeySet().Keys[0].Algorithm)
+ assert.Equal(t, "sig", p.GetKeySet().Keys[0].Use)
+ assert.Equal(t, "main-key", p.GetKeySet().Keys[0].KeyID)
+}
diff --git a/internal/oidc/store.go b/internal/oidc/store.go
new file mode 100644
index 00000000..0707ec78
--- /dev/null
+++ b/internal/oidc/store.go
@@ -0,0 +1,219 @@
+package oidc
+
+import (
+ "context"
+ "time"
+
+ "github.com/ory/fosite"
+ "github.com/ory/fosite/storage"
+ "gopkg.in/square/go-jose.v2"
+
+ "github.com/authelia/authelia/internal/authorization"
+ "github.com/authelia/authelia/internal/configuration/schema"
+ "github.com/authelia/authelia/internal/logging"
+)
+
+// NewOpenIDConnectStore returns a new OpenIDConnectStore using the provided schema.OpenIDConnectConfiguration.
+func NewOpenIDConnectStore(configuration *schema.OpenIDConnectConfiguration) (store *OpenIDConnectStore) {
+ store = &OpenIDConnectStore{}
+
+ store.clients = make(map[string]*InternalClient)
+
+ for _, clientConf := range configuration.Clients {
+ policy := authorization.PolicyToLevel(clientConf.Policy)
+ logging.Logger().Debugf("Registering client %s with policy %s (%v)", clientConf.ID, clientConf.Policy, policy)
+
+ client := &InternalClient{
+ ID: clientConf.ID,
+ Description: clientConf.Description,
+ Policy: authorization.PolicyToLevel(clientConf.Policy),
+ Secret: []byte(clientConf.Secret),
+ RedirectURIs: clientConf.RedirectURIs,
+ GrantTypes: clientConf.GrantTypes,
+ ResponseTypes: clientConf.ResponseTypes,
+ Scopes: clientConf.Scopes,
+ }
+
+ store.clients[client.ID] = client
+ }
+
+ store.memory = &storage.MemoryStore{
+ IDSessions: make(map[string]fosite.Requester),
+ Users: map[string]storage.MemoryUserRelation{},
+ AuthorizeCodes: map[string]storage.StoreAuthorizeCode{},
+ AccessTokens: map[string]fosite.Requester{},
+ RefreshTokens: map[string]storage.StoreRefreshToken{},
+ PKCES: map[string]fosite.Requester{},
+ AccessTokenRequestIDs: map[string]string{},
+ RefreshTokenRequestIDs: map[string]string{},
+ }
+
+ return store
+}
+
+// OpenIDConnectStore is Authelia's internal representation of the fosite.Storage interface.
+//
+// Currently it is mostly just implementing a decorator pattern other then GetInternalClient.
+// The long term plan is to have these methods interact with the Authelia storage and
+// session providers where applicable.
+type OpenIDConnectStore struct {
+ clients map[string]*InternalClient
+ memory *storage.MemoryStore
+}
+
+// GetClientPolicy retrieves the policy from the client with the matching provided id.
+func (s OpenIDConnectStore) GetClientPolicy(id string) (level authorization.Level) {
+ client, err := s.GetInternalClient(id)
+ if err != nil {
+ return authorization.TwoFactor
+ }
+
+ return client.Policy
+}
+
+// GetInternalClient returns a fosite.Client asserted as an InternalClient matching the provided id.
+func (s OpenIDConnectStore) GetInternalClient(id string) (client *InternalClient, err error) {
+ client, ok := s.clients[id]
+ if !ok {
+ return nil, fosite.ErrNotFound
+ }
+
+ return client, nil
+}
+
+// IsValidClientID returns true if the provided id exists in the OpenIDConnectProvider.Clients map.
+func (s OpenIDConnectStore) IsValidClientID(id string) (valid bool) {
+ _, err := s.GetInternalClient(id)
+
+ return err == nil
+}
+
+// CreateOpenIDConnectSession decorates fosite's storage.MemoryStore CreateOpenIDConnectSession method.
+func (s *OpenIDConnectStore) CreateOpenIDConnectSession(ctx context.Context, authorizeCode string, requester fosite.Requester) error {
+ return s.memory.CreateOpenIDConnectSession(ctx, authorizeCode, requester)
+}
+
+// GetOpenIDConnectSession decorates fosite's storage.MemoryStore GetOpenIDConnectSession method.
+func (s *OpenIDConnectStore) GetOpenIDConnectSession(ctx context.Context, authorizeCode string, requester fosite.Requester) (fosite.Requester, error) {
+ return s.memory.GetOpenIDConnectSession(ctx, authorizeCode, requester)
+}
+
+// DeleteOpenIDConnectSession decorates fosite's storage.MemoryStore DeleteOpenIDConnectSession method.
+func (s *OpenIDConnectStore) DeleteOpenIDConnectSession(ctx context.Context, authorizeCode string) error {
+ return s.memory.DeleteOpenIDConnectSession(ctx, authorizeCode)
+}
+
+// GetClient decorates fosite's storage.MemoryStore GetClient method.
+func (s *OpenIDConnectStore) GetClient(_ context.Context, id string) (fosite.Client, error) {
+ return s.GetInternalClient(id)
+}
+
+// ClientAssertionJWTValid decorates fosite's storage.MemoryStore ClientAssertionJWTValid method.
+func (s *OpenIDConnectStore) ClientAssertionJWTValid(ctx context.Context, jti string) error {
+ return s.memory.ClientAssertionJWTValid(ctx, jti)
+}
+
+// SetClientAssertionJWT decorates fosite's storage.MemoryStore SetClientAssertionJWT method.
+func (s *OpenIDConnectStore) SetClientAssertionJWT(ctx context.Context, jti string, exp time.Time) error {
+ return s.memory.SetClientAssertionJWT(ctx, jti, exp)
+}
+
+// CreateAuthorizeCodeSession decorates fosite's storage.MemoryStore CreateAuthorizeCodeSession method.
+func (s *OpenIDConnectStore) CreateAuthorizeCodeSession(ctx context.Context, code string, req fosite.Requester) error {
+ return s.memory.CreateAuthorizeCodeSession(ctx, code, req)
+}
+
+// GetAuthorizeCodeSession decorates fosite's storage.MemoryStore GetAuthorizeCodeSession method.
+func (s *OpenIDConnectStore) GetAuthorizeCodeSession(ctx context.Context, code string, session fosite.Session) (fosite.Requester, error) {
+ return s.memory.GetAuthorizeCodeSession(ctx, code, session)
+}
+
+// InvalidateAuthorizeCodeSession decorates fosite's storage.MemoryStore InvalidateAuthorizeCodeSession method.
+func (s *OpenIDConnectStore) InvalidateAuthorizeCodeSession(ctx context.Context, code string) error {
+ return s.memory.InvalidateAuthorizeCodeSession(ctx, code)
+}
+
+// CreatePKCERequestSession decorates fosite's storage.MemoryStore CreatePKCERequestSession method.
+func (s *OpenIDConnectStore) CreatePKCERequestSession(ctx context.Context, code string, req fosite.Requester) error {
+ return s.memory.CreatePKCERequestSession(ctx, code, req)
+}
+
+// GetPKCERequestSession decorates fosite's storage.MemoryStore GetPKCERequestSession method.
+func (s *OpenIDConnectStore) GetPKCERequestSession(ctx context.Context, code string, session fosite.Session) (fosite.Requester, error) {
+ return s.memory.GetPKCERequestSession(ctx, code, session)
+}
+
+// DeletePKCERequestSession decorates fosite's storage.MemoryStore DeletePKCERequestSession method.
+func (s *OpenIDConnectStore) DeletePKCERequestSession(ctx context.Context, code string) error {
+ return s.memory.DeletePKCERequestSession(ctx, code)
+}
+
+// CreateAccessTokenSession decorates fosite's storage.MemoryStore CreateAccessTokenSession method.
+func (s *OpenIDConnectStore) CreateAccessTokenSession(ctx context.Context, signature string, req fosite.Requester) error {
+ return s.memory.CreateAccessTokenSession(ctx, signature, req)
+}
+
+// GetAccessTokenSession decorates fosite's storage.MemoryStore GetAccessTokenSession method.
+func (s *OpenIDConnectStore) GetAccessTokenSession(ctx context.Context, signature string, session fosite.Session) (fosite.Requester, error) {
+ return s.memory.GetAccessTokenSession(ctx, signature, session)
+}
+
+// DeleteAccessTokenSession decorates fosite's storage.MemoryStore DeleteAccessTokenSession method.
+func (s *OpenIDConnectStore) DeleteAccessTokenSession(ctx context.Context, signature string) error {
+ return s.memory.DeleteAccessTokenSession(ctx, signature)
+}
+
+// CreateRefreshTokenSession decorates fosite's storage.MemoryStore CreateRefreshTokenSession method.
+func (s *OpenIDConnectStore) CreateRefreshTokenSession(ctx context.Context, signature string, req fosite.Requester) error {
+ return s.memory.CreateRefreshTokenSession(ctx, signature, req)
+}
+
+// GetRefreshTokenSession decorates fosite's storage.MemoryStore GetRefreshTokenSession method.
+func (s *OpenIDConnectStore) GetRefreshTokenSession(ctx context.Context, signature string, session fosite.Session) (fosite.Requester, error) {
+ return s.memory.GetRefreshTokenSession(ctx, signature, session)
+}
+
+// DeleteRefreshTokenSession decorates fosite's storage.MemoryStore DeleteRefreshTokenSession method.
+func (s *OpenIDConnectStore) DeleteRefreshTokenSession(ctx context.Context, signature string) error {
+ return s.memory.DeleteRefreshTokenSession(ctx, signature)
+}
+
+// Authenticate decorates fosite's storage.MemoryStore Authenticate method.
+func (s *OpenIDConnectStore) Authenticate(ctx context.Context, name string, secret string) error {
+ return s.memory.Authenticate(ctx, name, secret)
+}
+
+// RevokeRefreshToken decorates fosite's storage.MemoryStore RevokeRefreshToken method.
+func (s *OpenIDConnectStore) RevokeRefreshToken(ctx context.Context, requestID string) error {
+ return s.memory.RevokeRefreshToken(ctx, requestID)
+}
+
+// RevokeAccessToken decorates fosite's storage.MemoryStore RevokeAccessToken method.
+func (s *OpenIDConnectStore) RevokeAccessToken(ctx context.Context, requestID string) error {
+ return s.memory.RevokeAccessToken(ctx, requestID)
+}
+
+// GetPublicKey decorates fosite's storage.MemoryStore GetPublicKey method.
+func (s *OpenIDConnectStore) GetPublicKey(ctx context.Context, issuer string, subject string, keyID string) (*jose.JSONWebKey, error) {
+ return s.memory.GetPublicKey(ctx, issuer, subject, keyID)
+}
+
+// GetPublicKeys decorates fosite's storage.MemoryStore GetPublicKeys method.
+func (s *OpenIDConnectStore) GetPublicKeys(ctx context.Context, issuer string, subject string) (*jose.JSONWebKeySet, error) {
+ return s.memory.GetPublicKeys(ctx, issuer, subject)
+}
+
+// GetPublicKeyScopes decorates fosite's storage.MemoryStore GetPublicKeyScopes method.
+func (s *OpenIDConnectStore) GetPublicKeyScopes(ctx context.Context, issuer string, subject string, keyID string) ([]string, error) {
+ return s.memory.GetPublicKeyScopes(ctx, issuer, subject, keyID)
+}
+
+// IsJWTUsed decorates fosite's storage.MemoryStore IsJWTUsed method.
+func (s *OpenIDConnectStore) IsJWTUsed(ctx context.Context, jti string) (bool, error) {
+ return s.memory.IsJWTUsed(ctx, jti)
+}
+
+// MarkJWTUsedForTime decorates fosite's storage.MemoryStore MarkJWTUsedForTime method.
+func (s *OpenIDConnectStore) MarkJWTUsedForTime(ctx context.Context, jti string, exp time.Time) error {
+ return s.memory.MarkJWTUsedForTime(ctx, jti, exp)
+}
diff --git a/internal/oidc/store_test.go b/internal/oidc/store_test.go
new file mode 100644
index 00000000..62bab489
--- /dev/null
+++ b/internal/oidc/store_test.go
@@ -0,0 +1,132 @@
+package oidc
+
+import (
+ "context"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/authelia/authelia/internal/authorization"
+ "github.com/authelia/authelia/internal/configuration/schema"
+)
+
+func TestOpenIDConnectStore_GetClientPolicy(t *testing.T) {
+ s := NewOpenIDConnectStore(&schema.OpenIDConnectConfiguration{
+ IssuerPrivateKey: exampleIssuerPrivateKey,
+ Clients: []schema.OpenIDConnectClientConfiguration{
+ {
+ ID: "myclient",
+ Description: "myclient desc",
+ Policy: "one_factor",
+ Scopes: []string{"openid", "profile"},
+ Secret: "mysecret",
+ },
+ {
+ ID: "myotherclient",
+ Description: "myclient desc",
+ Policy: "two_factor",
+ Scopes: []string{"openid", "profile"},
+ Secret: "mysecret",
+ },
+ },
+ })
+
+ policyOne := s.GetClientPolicy("myclient")
+ assert.Equal(t, authorization.OneFactor, policyOne)
+
+ policyTwo := s.GetClientPolicy("myotherclient")
+ assert.Equal(t, authorization.TwoFactor, policyTwo)
+
+ policyInvalid := s.GetClientPolicy("invalidclient")
+ assert.Equal(t, authorization.TwoFactor, policyInvalid)
+}
+
+func TestOpenIDConnectStore_GetInternalClient(t *testing.T) {
+ s := NewOpenIDConnectStore(&schema.OpenIDConnectConfiguration{
+ IssuerPrivateKey: exampleIssuerPrivateKey,
+ Clients: []schema.OpenIDConnectClientConfiguration{
+ {
+ ID: "myclient",
+ Description: "myclient desc",
+ Policy: "one_factor",
+ Scopes: []string{"openid", "profile"},
+ Secret: "mysecret",
+ },
+ },
+ })
+
+ client, err := s.GetClient(context.Background(), "myinvalidclient")
+ assert.EqualError(t, err, "not_found")
+ assert.Nil(t, client)
+
+ client, err = s.GetClient(context.Background(), "myclient")
+ require.NoError(t, err)
+ require.NotNil(t, client)
+ assert.Equal(t, "myclient", client.GetID())
+}
+
+func TestOpenIDConnectStore_GetInternalClient_ValidClient(t *testing.T) {
+ c1 := schema.OpenIDConnectClientConfiguration{
+ ID: "myclient",
+ Description: "myclient desc",
+ Policy: "one_factor",
+ Scopes: []string{"openid", "profile"},
+ Secret: "mysecret",
+ }
+ s := NewOpenIDConnectStore(&schema.OpenIDConnectConfiguration{
+ IssuerPrivateKey: exampleIssuerPrivateKey,
+ Clients: []schema.OpenIDConnectClientConfiguration{c1},
+ })
+
+ client, err := s.GetInternalClient(c1.ID)
+ require.NoError(t, err)
+ require.NotNil(t, client)
+ assert.Equal(t, client.ID, c1.ID)
+ assert.Equal(t, client.Description, c1.Description)
+ assert.Equal(t, client.Scopes, c1.Scopes)
+ assert.Equal(t, client.GrantTypes, c1.GrantTypes)
+ assert.Equal(t, client.ResponseTypes, c1.ResponseTypes)
+ assert.Equal(t, client.RedirectURIs, c1.RedirectURIs)
+ assert.Equal(t, client.Policy, authorization.OneFactor)
+ assert.Equal(t, client.Secret, []byte(c1.Secret))
+}
+
+func TestOpenIDConnectStore_GetInternalClient_InvalidClient(t *testing.T) {
+ c1 := schema.OpenIDConnectClientConfiguration{
+ ID: "myclient",
+ Description: "myclient desc",
+ Policy: "one_factor",
+ Scopes: []string{"openid", "profile"},
+ Secret: "mysecret",
+ }
+ s := NewOpenIDConnectStore(&schema.OpenIDConnectConfiguration{
+ IssuerPrivateKey: exampleIssuerPrivateKey,
+ Clients: []schema.OpenIDConnectClientConfiguration{c1},
+ })
+
+ client, err := s.GetInternalClient("another-client")
+ assert.Nil(t, client)
+ assert.EqualError(t, err, "not_found")
+}
+
+func TestOpenIDConnectStore_IsValidClientID(t *testing.T) {
+ s := NewOpenIDConnectStore(&schema.OpenIDConnectConfiguration{
+ IssuerPrivateKey: exampleIssuerPrivateKey,
+ Clients: []schema.OpenIDConnectClientConfiguration{
+ {
+ ID: "myclient",
+ Description: "myclient desc",
+ Policy: "one_factor",
+ Scopes: []string{"openid", "profile"},
+ Secret: "mysecret",
+ },
+ },
+ })
+
+ validClient := s.IsValidClientID("myclient")
+ invalidClient := s.IsValidClientID("myinvalidclient")
+
+ assert.True(t, validClient)
+ assert.False(t, invalidClient)
+}
diff --git a/internal/server/server.go b/internal/server/server.go
index 60aff102..44557706 100644
--- a/internal/server/server.go
+++ b/internal/server/server.go
@@ -28,9 +28,7 @@ import (
//go:embed public_html
var assets embed.FS
-// StartServer start Authelia server with the given configuration and providers.
-func StartServer(configuration schema.Configuration, providers middlewares.Providers) {
- logger := logging.Logger()
+func registerRoutes(configuration schema.Configuration, providers middlewares.Providers) fasthttp.RequestHandler {
autheliaMiddleware := middlewares.AutheliaMiddleware(configuration, providers)
rememberMe := strconv.FormatBool(configuration.Session.RememberMeDuration != "0")
resetPassword := strconv.FormatBool(!configuration.AuthenticationBackend.DisableResetPassword)
@@ -142,6 +140,19 @@ func StartServer(configuration schema.Configuration, providers middlewares.Provi
handler = middlewares.StripPathMiddleware(handler)
}
+ if providers.OpenIDConnect.Fosite != nil {
+ handlers.RegisterOIDC(r, autheliaMiddleware)
+ }
+
+ return handler
+}
+
+// StartServer start Authelia server with the given configuration and providers.
+func StartServer(configuration schema.Configuration, providers middlewares.Providers) {
+ logger := logging.Logger()
+
+ handler := registerRoutes(configuration, providers)
+
server := &fasthttp.Server{
ErrorHandler: autheliaErrorHandler,
Handler: handler,
@@ -157,6 +168,7 @@ func StartServer(configuration schema.Configuration, providers middlewares.Provi
logger.Fatalf("Error initializing listener: %s", err)
}
+ // TODO(clems4ever): move that piece to a more related location, probably in the configuration package.
if configuration.AuthenticationBackend.File != nil && configuration.AuthenticationBackend.File.Password.Algorithm == "argon2id" && runtime.GOOS == "linux" {
f, err := ioutil.ReadFile("/sys/fs/cgroup/memory/memory.limit_in_bytes")
if err != nil {
diff --git a/internal/session/provider.go b/internal/session/provider.go
index a8544ae5..df9036fb 100644
--- a/internal/session/provider.go
+++ b/internal/session/provider.go
@@ -87,6 +87,7 @@ func (p *Provider) GetSession(ctx *fasthttp.RequestCtx) (UserSession, error) {
// and save it in the store.
if !ok {
userSession := NewDefaultUserSession()
+
store.Set(userSessionStorerKey, userSession)
return userSession, nil
@@ -130,6 +131,7 @@ func (p *Provider) SaveSession(ctx *fasthttp.RequestCtx, userSession UserSession
// RegenerateSession regenerate a session ID.
func (p *Provider) RegenerateSession(ctx *fasthttp.RequestCtx) error {
err := p.sessionHolder.Regenerate(ctx)
+
return err
}
diff --git a/internal/session/types.go b/internal/session/types.go
index de2649f3..3ebe30da 100644
--- a/internal/session/types.go
+++ b/internal/session/types.go
@@ -8,6 +8,7 @@ import (
"github.com/tstranex/u2f"
"github.com/authelia/authelia/internal/authentication"
+ "github.com/authelia/authelia/internal/authorization"
)
// ProviderConfig is the configuration used to create the session provider.
@@ -43,6 +44,9 @@ type UserSession struct {
// This is used in second phase of a U2F authentication.
U2FRegistration *U2FRegistration
+ // Represent an OIDC workflow session initiated by the client if not null.
+ OIDCWorkflowSession *OIDCWorkflowSession
+
// This boolean is set to true after identity verification and checked
// while doing the query actually updating the password.
PasswordResetUsername *string
@@ -55,3 +59,15 @@ type Identity struct {
Username string
Email string
}
+
+// OIDCWorkflowSession represent an OIDC workflow session.
+type OIDCWorkflowSession struct {
+ ClientID string
+ RequestedScopes []string
+ GrantedScopes []string
+ RequestedAudience []string
+ GrantedAudience []string
+ TargetURI string
+ AuthURI string
+ RequiredAuthorizationLevel authorization.Level
+}
diff --git a/internal/suites/OIDC/configuration.yml b/internal/suites/OIDC/configuration.yml
new file mode 100644
index 00000000..44569ce3
--- /dev/null
+++ b/internal/suites/OIDC/configuration.yml
@@ -0,0 +1,99 @@
+---
+port: 9091
+tls_cert: /config/ssl/cert.pem
+tls_key: /config/ssl/key.pem
+
+log_level: debug
+
+jwt_secret: unsecure_secret
+
+authentication_backend:
+ file:
+ path: /config/users.yml
+
+session:
+ secret: unsecure_session_secret
+ domain: example.com
+ expiration: 3600 # 1 hour
+ inactivity: 300 # 5 minutes
+ remember_me_duration: 1y
+ # We use redis here to keep the users authenticated when Authelia restarts
+ # It eases development.
+ redis:
+ host: redis
+ port: 6379
+
+storage:
+ local:
+ path: /config/db.sqlite
+
+access_control:
+ default_policy: deny
+ rules:
+ - domain: "home.example.com"
+ policy: bypass
+ - domain: "public.example.com"
+ policy: bypass
+ - domain: "admin.example.com"
+ policy: two_factor
+ - domain: "secure.example.com"
+ policy: two_factor
+ - domain: "singlefactor.example.com"
+ policy: one_factor
+ - domain: "oidc.example.com"
+ policy: two_factor
+ - domain: "oidc-public.example.com"
+ policy: bypass
+
+notifier:
+ smtp:
+ host: smtp
+ port: 1025
+ sender: admin@example.com
+ disable_require_tls: true
+
+identity_providers:
+ oidc:
+ hmac_secret: IVPWBkAdJHje3uz7LtFTDU2pFUfh39Xm
+ issuer_private_key: |
+ -----BEGIN RSA PRIVATE KEY-----
+ MIIEogIBAAKCAQEAvOFmoEJFt1JkfdlwM3vJFg5rrY9d6LyyqezjZkBZDQ4qdEEU
+ dCrbW8ISFTtg9sfbrS3qingUzVP9VOfYPMC3r0ugjJXjhvJdBSaoLlzL3saeyrXk
+ frOOvkcWKzeOynqUNPhKy9dchmuLALFfd/Jy7Wzq0y7XxGeNidEmFjMAf9dwf6/+
+ PjQjbG7zBFu/XSajITPHlDXPVDd0j2qw2wu5Z9iqn4LRXnAFnC438hZZKZU/+JxU
+ 2ezr6Sefiy8XTC2kDiq3cgLeEjSywlJOs+4TLjVS/3h75sh2Wk0xVaSwjPEjCOgm
+ a+2E3GJrGdQBiAjMSu101VBVwHUHaLDCn1T4NwIDAQABAoIBADWkupXnXI99Ogc4
+ GxK0JF88Rz6qyhwQg5mZKthejCwWCt6roRiBF33O933KOHa+OljMAqHDCv1pzjgw
+ BIz0mvaRPw7OfylTajHNUdShDFHADVc7I6MMcgz+eYBarhY5jCAjKHMOPjv7DSZs
+ OdYCKLvfxC2oTyV714n9uZhyccDcvQpkgZuBDL0oxPom1GOI8TGhPjxvFOovEHWA
+ Q8q9XY4cUVNDikZmvpgeUkJHWYHYb+11vKeSupnYD03yJ3sDy+F6+m+3/XmzFbXb
+ 1p43ermHQsMfDlxPyulUUI0viSo2UhlMC/moAb9FusOv+dTl2lt0gGqzDJ9gg1z1
+ XpHRnwkCgYEA5x48dyxd4lydtVYef9sBmbLJEYozsYyOwLcnrLSNaZxeCza1exyR
+ QIRogswoLDacxrYvO8FY6LtAEMkisv732M29zthBPm5wyoSZiM1X2YfQXKsmyh2h
+ x1/yCWv/BQjj68A8IAxToaXxSG4WAr/X00RGUkXgkgw122FxcmGuFyUCgYEA0TcR
+ dnt/oRMK4aCZHcBgTknzDfxKlJh4S0C9WjxKgr8IlW4LTeVSBuuqOObOQYImEhtw
+ TRTKZIViL0roDF79cioQSp1Tk5h6uy8wr6VyhWRnWfTz2/azoTHnmQ780rtAuEI/
+ NvE6FiqwikJLjma1YJoRfr/bfmgMdxcYbJI1MSsCgYAEZ5Yda1IKu1siFpcUNrdM
+ F5UvaWPc0WHzGEqARxye06UTL6K7yuqVwTBAteVaGlxYiSZTTDcGkHMDHuIzaRqO
+ HjWs2IA90VsC8Q4ABnHTKnx1F6nwlin8I774IP/GN8ooNwyuS63YWdJEYBy5RrC1
+ TQrODJjgD62DFdNUq7nmpQKBgFMJEzI+Q+KPJ0NztTG8t7x61y/W0Vb2yM+9Syn0
+ QfJwlZyRR4VMHelHQZFB8dzIJgoLv9+n/8gztEtm5IB8dwUHst2aYaBz5UpDqYQd
+ Gz3cIrTuZpcH7DVvFCeIbknJLh+zk1lgFpjTqqvFMi27kANeQtFWnmwmKcRec0As
+ K1ZvAoGAV/3YB44/zIoB590+yhpx2HTmDPVHH+J+5O71Pi1D9W13ClBFLrE69wo+
+ IQLIstBI5tGOGeuQNjXhDKJ1U30xppZXcnebrkA+oOo+6dy20zghFR2maAGXfWFU
+ pM4GsSnSTm0bXPebVouQFqhj7LqcQQzCqRDThmw/Lp1tJUmu40g=
+ -----END RSA PRIVATE KEY-----
+ clients:
+ - id: oidc-tester-app
+ secret: foobar
+ policy: two_factor
+ redirect_uris:
+ - https://oidc.example.com:8080/oauth2/callback
+ # This client is used for testing purpose. As of now, the app must be protected by ACLs
+ # otherwise it won't work properly.
+ - id: oidc-tester-app-public
+ secret: foobar
+ authorization_policy: one_factor
+ redirect_uris:
+ - https://oidc-public.example.com:8080/oauth2/callback
+...
diff --git a/internal/suites/OIDC/docker-compose.yml b/internal/suites/OIDC/docker-compose.yml
new file mode 100644
index 00000000..2da124b7
--- /dev/null
+++ b/internal/suites/OIDC/docker-compose.yml
@@ -0,0 +1,10 @@
+---
+version: '3'
+services:
+ authelia-backend:
+ volumes:
+ - './OIDC/configuration.yml:/config/configuration.yml:ro'
+ - './OIDC/users.yml:/config/users.yml'
+ - './OIDC/keypair/key.pem:/config/issuer.pem:ro'
+ - './common/ssl:/config/ssl:ro'
+...
diff --git a/internal/suites/OIDC/keypair/key.pem b/internal/suites/OIDC/keypair/key.pem
new file mode 100644
index 00000000..c5df003c
--- /dev/null
+++ b/internal/suites/OIDC/keypair/key.pem
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEogIBAAKCAQEAvOFmoEJFt1JkfdlwM3vJFg5rrY9d6LyyqezjZkBZDQ4qdEEU
+dCrbW8ISFTtg9sfbrS3qingUzVP9VOfYPMC3r0ugjJXjhvJdBSaoLlzL3saeyrXk
+frOOvkcWKzeOynqUNPhKy9dchmuLALFfd/Jy7Wzq0y7XxGeNidEmFjMAf9dwf6/+
+PjQjbG7zBFu/XSajITPHlDXPVDd0j2qw2wu5Z9iqn4LRXnAFnC438hZZKZU/+JxU
+2ezr6Sefiy8XTC2kDiq3cgLeEjSywlJOs+4TLjVS/3h75sh2Wk0xVaSwjPEjCOgm
+a+2E3GJrGdQBiAjMSu101VBVwHUHaLDCn1T4NwIDAQABAoIBADWkupXnXI99Ogc4
+GxK0JF88Rz6qyhwQg5mZKthejCwWCt6roRiBF33O933KOHa+OljMAqHDCv1pzjgw
+BIz0mvaRPw7OfylTajHNUdShDFHADVc7I6MMcgz+eYBarhY5jCAjKHMOPjv7DSZs
+OdYCKLvfxC2oTyV714n9uZhyccDcvQpkgZuBDL0oxPom1GOI8TGhPjxvFOovEHWA
+Q8q9XY4cUVNDikZmvpgeUkJHWYHYb+11vKeSupnYD03yJ3sDy+F6+m+3/XmzFbXb
+1p43ermHQsMfDlxPyulUUI0viSo2UhlMC/moAb9FusOv+dTl2lt0gGqzDJ9gg1z1
+XpHRnwkCgYEA5x48dyxd4lydtVYef9sBmbLJEYozsYyOwLcnrLSNaZxeCza1exyR
+QIRogswoLDacxrYvO8FY6LtAEMkisv732M29zthBPm5wyoSZiM1X2YfQXKsmyh2h
+x1/yCWv/BQjj68A8IAxToaXxSG4WAr/X00RGUkXgkgw122FxcmGuFyUCgYEA0TcR
+dnt/oRMK4aCZHcBgTknzDfxKlJh4S0C9WjxKgr8IlW4LTeVSBuuqOObOQYImEhtw
+TRTKZIViL0roDF79cioQSp1Tk5h6uy8wr6VyhWRnWfTz2/azoTHnmQ780rtAuEI/
+NvE6FiqwikJLjma1YJoRfr/bfmgMdxcYbJI1MSsCgYAEZ5Yda1IKu1siFpcUNrdM
+F5UvaWPc0WHzGEqARxye06UTL6K7yuqVwTBAteVaGlxYiSZTTDcGkHMDHuIzaRqO
+HjWs2IA90VsC8Q4ABnHTKnx1F6nwlin8I774IP/GN8ooNwyuS63YWdJEYBy5RrC1
+TQrODJjgD62DFdNUq7nmpQKBgFMJEzI+Q+KPJ0NztTG8t7x61y/W0Vb2yM+9Syn0
+QfJwlZyRR4VMHelHQZFB8dzIJgoLv9+n/8gztEtm5IB8dwUHst2aYaBz5UpDqYQd
+Gz3cIrTuZpcH7DVvFCeIbknJLh+zk1lgFpjTqqvFMi27kANeQtFWnmwmKcRec0As
+K1ZvAoGAV/3YB44/zIoB590+yhpx2HTmDPVHH+J+5O71Pi1D9W13ClBFLrE69wo+
+IQLIstBI5tGOGeuQNjXhDKJ1U30xppZXcnebrkA+oOo+6dy20zghFR2maAGXfWFU
+pM4GsSnSTm0bXPebVouQFqhj7LqcQQzCqRDThmw/Lp1tJUmu40g=
+-----END RSA PRIVATE KEY-----
diff --git a/internal/suites/OIDC/keypair/key.pub b/internal/suites/OIDC/keypair/key.pub
new file mode 100644
index 00000000..b8e37ffa
--- /dev/null
+++ b/internal/suites/OIDC/keypair/key.pub
@@ -0,0 +1,9 @@
+-----BEGIN RSA PUBLIC KEY-----
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvOFmoEJFt1JkfdlwM3vJ
+Fg5rrY9d6LyyqezjZkBZDQ4qdEEUdCrbW8ISFTtg9sfbrS3qingUzVP9VOfYPMC3
+r0ugjJXjhvJdBSaoLlzL3saeyrXkfrOOvkcWKzeOynqUNPhKy9dchmuLALFfd/Jy
+7Wzq0y7XxGeNidEmFjMAf9dwf6/+PjQjbG7zBFu/XSajITPHlDXPVDd0j2qw2wu5
+Z9iqn4LRXnAFnC438hZZKZU/+JxU2ezr6Sefiy8XTC2kDiq3cgLeEjSywlJOs+4T
+LjVS/3h75sh2Wk0xVaSwjPEjCOgma+2E3GJrGdQBiAjMSu101VBVwHUHaLDCn1T4
+NwIDAQAB
+-----END RSA PUBLIC KEY-----
diff --git a/internal/suites/OIDC/users.yml b/internal/suites/OIDC/users.yml
new file mode 100644
index 00000000..a52978b2
--- /dev/null
+++ b/internal/suites/OIDC/users.yml
@@ -0,0 +1,35 @@
+---
+###############################################################
+# Users Database #
+###############################################################
+
+# This file can be used if you do not have an LDAP set up.
+
+# List of users
+users:
+ john:
+ displayname: "John Doe"
+ password: "$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/" # yamllint disable-line rule:line-length
+ email: john.doe@authelia.com
+ groups:
+ - admins
+ - dev
+
+ harry:
+ displayname: "Harry Potter"
+ password: "$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/" # yamllint disable-line rule:line-length
+ email: harry.potter@authelia.com
+ groups: []
+
+ bob:
+ displayname: "Bob Dylan"
+ password: "$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/" # yamllint disable-line rule:line-length
+ email: bob.dylan@authelia.com
+ groups:
+ - dev
+
+ james:
+ displayname: "James Dean"
+ password: "$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/" # yamllint disable-line rule:line-length
+ email: james.dean@authelia.com
+...
diff --git a/internal/suites/OIDCTraefik/configuration.yml b/internal/suites/OIDCTraefik/configuration.yml
new file mode 100644
index 00000000..7ad402bd
--- /dev/null
+++ b/internal/suites/OIDCTraefik/configuration.yml
@@ -0,0 +1,101 @@
+---
+port: 9091
+tls_cert: /config/ssl/cert.pem
+tls_key: /config/ssl/key.pem
+
+log_level: debug
+
+jwt_secret: unsecure_secret
+
+authentication_backend:
+ file:
+ path: /config/users.yml
+
+session:
+ secret: unsecure_session_secret
+ domain: example.com
+ expiration: 3600 # 1 hour
+ inactivity: 300 # 5 minutes
+ remember_me_duration: 1y
+ # We use redis here to keep the users authenticated when Authelia restarts
+ # It eases development.
+ redis:
+ host: redis
+ port: 6379
+
+storage:
+ local:
+ path: /config/db.sqlite
+
+access_control:
+ default_policy: deny
+ rules:
+ - domain: "home.example.com"
+ policy: bypass
+ - domain: "public.example.com"
+ policy: bypass
+ - domain: "admin.example.com"
+ policy: two_factor
+ - domain: "secure.example.com"
+ policy: two_factor
+ - domain: "singlefactor.example.com"
+ policy: one_factor
+ - domain: "oidc.example.com"
+ policy: two_factor
+ - domain: "oidc-public.example.com"
+ policy: bypass
+ - domain: "traefik.example.com"
+ policy: bypass
+
+notifier:
+ smtp:
+ host: smtp
+ port: 1025
+ sender: admin@example.com
+ disable_require_tls: true
+
+identity_providers:
+ oidc:
+ hmac_secret: IVPWBkAdJHje3uz7LtFTDU2pFUfh39Xm
+ issuer_private_key: |
+ -----BEGIN RSA PRIVATE KEY-----
+ MIIEogIBAAKCAQEAvOFmoEJFt1JkfdlwM3vJFg5rrY9d6LyyqezjZkBZDQ4qdEEU
+ dCrbW8ISFTtg9sfbrS3qingUzVP9VOfYPMC3r0ugjJXjhvJdBSaoLlzL3saeyrXk
+ frOOvkcWKzeOynqUNPhKy9dchmuLALFfd/Jy7Wzq0y7XxGeNidEmFjMAf9dwf6/+
+ PjQjbG7zBFu/XSajITPHlDXPVDd0j2qw2wu5Z9iqn4LRXnAFnC438hZZKZU/+JxU
+ 2ezr6Sefiy8XTC2kDiq3cgLeEjSywlJOs+4TLjVS/3h75sh2Wk0xVaSwjPEjCOgm
+ a+2E3GJrGdQBiAjMSu101VBVwHUHaLDCn1T4NwIDAQABAoIBADWkupXnXI99Ogc4
+ GxK0JF88Rz6qyhwQg5mZKthejCwWCt6roRiBF33O933KOHa+OljMAqHDCv1pzjgw
+ BIz0mvaRPw7OfylTajHNUdShDFHADVc7I6MMcgz+eYBarhY5jCAjKHMOPjv7DSZs
+ OdYCKLvfxC2oTyV714n9uZhyccDcvQpkgZuBDL0oxPom1GOI8TGhPjxvFOovEHWA
+ Q8q9XY4cUVNDikZmvpgeUkJHWYHYb+11vKeSupnYD03yJ3sDy+F6+m+3/XmzFbXb
+ 1p43ermHQsMfDlxPyulUUI0viSo2UhlMC/moAb9FusOv+dTl2lt0gGqzDJ9gg1z1
+ XpHRnwkCgYEA5x48dyxd4lydtVYef9sBmbLJEYozsYyOwLcnrLSNaZxeCza1exyR
+ QIRogswoLDacxrYvO8FY6LtAEMkisv732M29zthBPm5wyoSZiM1X2YfQXKsmyh2h
+ x1/yCWv/BQjj68A8IAxToaXxSG4WAr/X00RGUkXgkgw122FxcmGuFyUCgYEA0TcR
+ dnt/oRMK4aCZHcBgTknzDfxKlJh4S0C9WjxKgr8IlW4LTeVSBuuqOObOQYImEhtw
+ TRTKZIViL0roDF79cioQSp1Tk5h6uy8wr6VyhWRnWfTz2/azoTHnmQ780rtAuEI/
+ NvE6FiqwikJLjma1YJoRfr/bfmgMdxcYbJI1MSsCgYAEZ5Yda1IKu1siFpcUNrdM
+ F5UvaWPc0WHzGEqARxye06UTL6K7yuqVwTBAteVaGlxYiSZTTDcGkHMDHuIzaRqO
+ HjWs2IA90VsC8Q4ABnHTKnx1F6nwlin8I774IP/GN8ooNwyuS63YWdJEYBy5RrC1
+ TQrODJjgD62DFdNUq7nmpQKBgFMJEzI+Q+KPJ0NztTG8t7x61y/W0Vb2yM+9Syn0
+ QfJwlZyRR4VMHelHQZFB8dzIJgoLv9+n/8gztEtm5IB8dwUHst2aYaBz5UpDqYQd
+ Gz3cIrTuZpcH7DVvFCeIbknJLh+zk1lgFpjTqqvFMi27kANeQtFWnmwmKcRec0As
+ K1ZvAoGAV/3YB44/zIoB590+yhpx2HTmDPVHH+J+5O71Pi1D9W13ClBFLrE69wo+
+ IQLIstBI5tGOGeuQNjXhDKJ1U30xppZXcnebrkA+oOo+6dy20zghFR2maAGXfWFU
+ pM4GsSnSTm0bXPebVouQFqhj7LqcQQzCqRDThmw/Lp1tJUmu40g=
+ -----END RSA PRIVATE KEY-----
+ clients:
+ - id: oidc-tester-app
+ secret: foobar
+ policy: two_factor
+ redirect_uris:
+ - https://oidc.example.com:8080/oauth2/callback
+ # This client is used for testing purpose. As of now, the app must be protected by ACLs
+ # otherwise it won't work properly.
+ - id: oidc-tester-app-public
+ secret: foobar
+ authorization_policy: one_factor
+ redirect_uris:
+ - https://oidc-public.example.com:8080/oauth2/callback
+...
diff --git a/internal/suites/OIDCTraefik/docker-compose.yml b/internal/suites/OIDCTraefik/docker-compose.yml
new file mode 100644
index 00000000..a5f4dd82
--- /dev/null
+++ b/internal/suites/OIDCTraefik/docker-compose.yml
@@ -0,0 +1,10 @@
+---
+version: '3'
+services:
+ authelia-backend:
+ volumes:
+ - './OIDCTraefik/configuration.yml:/config/configuration.yml:ro'
+ - './OIDCTraefik/users.yml:/config/users.yml'
+ - './OIDCTraefik/keypair/key.pem:/config/issuer.pem:ro'
+ - './common/ssl:/config/ssl:ro'
+...
diff --git a/internal/suites/OIDCTraefik/keypair/key.pem b/internal/suites/OIDCTraefik/keypair/key.pem
new file mode 100644
index 00000000..c5df003c
--- /dev/null
+++ b/internal/suites/OIDCTraefik/keypair/key.pem
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEogIBAAKCAQEAvOFmoEJFt1JkfdlwM3vJFg5rrY9d6LyyqezjZkBZDQ4qdEEU
+dCrbW8ISFTtg9sfbrS3qingUzVP9VOfYPMC3r0ugjJXjhvJdBSaoLlzL3saeyrXk
+frOOvkcWKzeOynqUNPhKy9dchmuLALFfd/Jy7Wzq0y7XxGeNidEmFjMAf9dwf6/+
+PjQjbG7zBFu/XSajITPHlDXPVDd0j2qw2wu5Z9iqn4LRXnAFnC438hZZKZU/+JxU
+2ezr6Sefiy8XTC2kDiq3cgLeEjSywlJOs+4TLjVS/3h75sh2Wk0xVaSwjPEjCOgm
+a+2E3GJrGdQBiAjMSu101VBVwHUHaLDCn1T4NwIDAQABAoIBADWkupXnXI99Ogc4
+GxK0JF88Rz6qyhwQg5mZKthejCwWCt6roRiBF33O933KOHa+OljMAqHDCv1pzjgw
+BIz0mvaRPw7OfylTajHNUdShDFHADVc7I6MMcgz+eYBarhY5jCAjKHMOPjv7DSZs
+OdYCKLvfxC2oTyV714n9uZhyccDcvQpkgZuBDL0oxPom1GOI8TGhPjxvFOovEHWA
+Q8q9XY4cUVNDikZmvpgeUkJHWYHYb+11vKeSupnYD03yJ3sDy+F6+m+3/XmzFbXb
+1p43ermHQsMfDlxPyulUUI0viSo2UhlMC/moAb9FusOv+dTl2lt0gGqzDJ9gg1z1
+XpHRnwkCgYEA5x48dyxd4lydtVYef9sBmbLJEYozsYyOwLcnrLSNaZxeCza1exyR
+QIRogswoLDacxrYvO8FY6LtAEMkisv732M29zthBPm5wyoSZiM1X2YfQXKsmyh2h
+x1/yCWv/BQjj68A8IAxToaXxSG4WAr/X00RGUkXgkgw122FxcmGuFyUCgYEA0TcR
+dnt/oRMK4aCZHcBgTknzDfxKlJh4S0C9WjxKgr8IlW4LTeVSBuuqOObOQYImEhtw
+TRTKZIViL0roDF79cioQSp1Tk5h6uy8wr6VyhWRnWfTz2/azoTHnmQ780rtAuEI/
+NvE6FiqwikJLjma1YJoRfr/bfmgMdxcYbJI1MSsCgYAEZ5Yda1IKu1siFpcUNrdM
+F5UvaWPc0WHzGEqARxye06UTL6K7yuqVwTBAteVaGlxYiSZTTDcGkHMDHuIzaRqO
+HjWs2IA90VsC8Q4ABnHTKnx1F6nwlin8I774IP/GN8ooNwyuS63YWdJEYBy5RrC1
+TQrODJjgD62DFdNUq7nmpQKBgFMJEzI+Q+KPJ0NztTG8t7x61y/W0Vb2yM+9Syn0
+QfJwlZyRR4VMHelHQZFB8dzIJgoLv9+n/8gztEtm5IB8dwUHst2aYaBz5UpDqYQd
+Gz3cIrTuZpcH7DVvFCeIbknJLh+zk1lgFpjTqqvFMi27kANeQtFWnmwmKcRec0As
+K1ZvAoGAV/3YB44/zIoB590+yhpx2HTmDPVHH+J+5O71Pi1D9W13ClBFLrE69wo+
+IQLIstBI5tGOGeuQNjXhDKJ1U30xppZXcnebrkA+oOo+6dy20zghFR2maAGXfWFU
+pM4GsSnSTm0bXPebVouQFqhj7LqcQQzCqRDThmw/Lp1tJUmu40g=
+-----END RSA PRIVATE KEY-----
diff --git a/internal/suites/OIDCTraefik/keypair/key.pub b/internal/suites/OIDCTraefik/keypair/key.pub
new file mode 100644
index 00000000..b8e37ffa
--- /dev/null
+++ b/internal/suites/OIDCTraefik/keypair/key.pub
@@ -0,0 +1,9 @@
+-----BEGIN RSA PUBLIC KEY-----
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvOFmoEJFt1JkfdlwM3vJ
+Fg5rrY9d6LyyqezjZkBZDQ4qdEEUdCrbW8ISFTtg9sfbrS3qingUzVP9VOfYPMC3
+r0ugjJXjhvJdBSaoLlzL3saeyrXkfrOOvkcWKzeOynqUNPhKy9dchmuLALFfd/Jy
+7Wzq0y7XxGeNidEmFjMAf9dwf6/+PjQjbG7zBFu/XSajITPHlDXPVDd0j2qw2wu5
+Z9iqn4LRXnAFnC438hZZKZU/+JxU2ezr6Sefiy8XTC2kDiq3cgLeEjSywlJOs+4T
+LjVS/3h75sh2Wk0xVaSwjPEjCOgma+2E3GJrGdQBiAjMSu101VBVwHUHaLDCn1T4
+NwIDAQAB
+-----END RSA PUBLIC KEY-----
diff --git a/internal/suites/OIDCTraefik/users.yml b/internal/suites/OIDCTraefik/users.yml
new file mode 100644
index 00000000..a52978b2
--- /dev/null
+++ b/internal/suites/OIDCTraefik/users.yml
@@ -0,0 +1,35 @@
+---
+###############################################################
+# Users Database #
+###############################################################
+
+# This file can be used if you do not have an LDAP set up.
+
+# List of users
+users:
+ john:
+ displayname: "John Doe"
+ password: "$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/" # yamllint disable-line rule:line-length
+ email: john.doe@authelia.com
+ groups:
+ - admins
+ - dev
+
+ harry:
+ displayname: "Harry Potter"
+ password: "$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/" # yamllint disable-line rule:line-length
+ email: harry.potter@authelia.com
+ groups: []
+
+ bob:
+ displayname: "Bob Dylan"
+ password: "$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/" # yamllint disable-line rule:line-length
+ email: bob.dylan@authelia.com
+ groups:
+ - dev
+
+ james:
+ displayname: "James Dean"
+ password: "$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/" # yamllint disable-line rule:line-length
+ email: james.dean@authelia.com
+...
diff --git a/internal/suites/const.go b/internal/suites/const.go
index 2053161e..d1300159 100644
--- a/internal/suites/const.go
+++ b/internal/suites/const.go
@@ -41,6 +41,9 @@ var MX1MailBaseURL = fmt.Sprintf("https://mx1.mail.%s", BaseDomain)
// MX2MailBaseURL the base URL of the mx2.mail domain.
var MX2MailBaseURL = fmt.Sprintf("https://mx2.mail.%s", BaseDomain)
+// OIDCBaseURL the base URL of the oidc domain.
+var OIDCBaseURL = fmt.Sprintf("https://oidc.%s", BaseDomain)
+
// DuoBaseURL the base URL of the Duo configuration API.
var DuoBaseURL = "https://duo.example.com"
diff --git a/internal/suites/docker.go b/internal/suites/docker.go
index 2c978766..cb5349d3 100644
--- a/internal/suites/docker.go
+++ b/internal/suites/docker.go
@@ -45,6 +45,11 @@ func (de *DockerEnvironment) createCommand(cmd string) *exec.Cmd {
return utils.Command("bash", "-c", dockerCmdLine)
}
+// Pull pull all images of needed in the environment.
+func (de *DockerEnvironment) Pull(images ...string) error {
+ return de.createCommandWithStdout(fmt.Sprintf("pull %s", strings.Join(images, " "))).Run()
+}
+
// Up spawn a docker environment.
func (de *DockerEnvironment) Up() error {
return de.createCommandWithStdout("up --build -d").Run()
diff --git a/internal/suites/example/compose/authelia/docker-compose.backend.dev.yml b/internal/suites/example/compose/authelia/docker-compose.backend.dev.yml
index 099af651..8fe1be0a 100644
--- a/internal/suites/example/compose/authelia/docker-compose.backend.dev.yml
+++ b/internal/suites/example/compose/authelia/docker-compose.backend.dev.yml
@@ -24,7 +24,7 @@ services:
- 'traefik.frontend.rule=Host:login.example.com;PathPrefix:/api'
- 'traefik.protocol=https'
# Traefik 2.x
- - 'traefik.http.routers.authelia_backend.rule=Host(`login.example.com`) && PathPrefix(`/api`) || Host(`login.example.com`) && PathPrefix(`${PathPrefix}/api/`)' # yamllint disable-line rule:line-length
+ - 'traefik.http.routers.authelia_backend.rule=Host(`login.example.com`) && PathPrefix(`/.well-known/openid-configuration`) || Host(`login.example.com`) && PathPrefix(`/api`) || Host(`login.example.com`) && PathPrefix(`${PathPrefix}/api/`)' # yamllint disable-line rule:line-length
- 'traefik.http.routers.authelia_backend.entrypoints=https'
- 'traefik.http.routers.authelia_backend.tls=true'
- 'traefik.http.services.authelia_backend.loadbalancer.server.scheme=https'
diff --git a/internal/suites/example/compose/authelia/docker-compose.backend.dist.yml b/internal/suites/example/compose/authelia/docker-compose.backend.dist.yml
index 9a00dbce..524f8c1d 100644
--- a/internal/suites/example/compose/authelia/docker-compose.backend.dist.yml
+++ b/internal/suites/example/compose/authelia/docker-compose.backend.dist.yml
@@ -8,10 +8,11 @@ services:
- 'traefik.frontend.rule=Host:login.example.com;PathPrefix:/api'
- 'traefik.protocol=https'
# Traefik 2.x
- - 'traefik.http.routers.authelia_backend.rule=Host(`login.example.com`) && PathPrefix(`/api`) || Host(`login.example.com`) && PathPrefix(`${PathPrefix}/api/`)' # yamllint disable-line rule:line-length
+ - 'traefik.http.routers.authelia_backend.rule=Host(`login.example.com`) && PathPrefix(`/.well-known/openid-configuration`) || Host(`login.example.com`) && PathPrefix(`/api`) || Host(`login.example.com`) && PathPrefix(`${PathPrefix}/api/`)' # yamllint disable-line rule:line-length
- 'traefik.http.routers.authelia_backend.entrypoints=https'
- 'traefik.http.routers.authelia_backend.tls=true'
- 'traefik.http.services.authelia_backend.loadbalancer.server.scheme=https'
+ - 'traefik.http.services.authelia_backend.passHostHeader=true'
volumes:
- '../..:/authelia'
environment:
diff --git a/internal/suites/example/compose/nginx/backend/html/home/index.html b/internal/suites/example/compose/nginx/backend/html/home/index.html
index 1178229a..2eea6802 100644
--- a/internal/suites/example/compose/nginx/backend/html/home/index.html
+++ b/internal/suites/example/compose/nginx/backend/html/home/index.html
@@ -58,6 +58,9 @@
mx2.main.example.com / secret.html
+
+ oidc.example.com / (only in OIDC suite).
+
You can also log off by visiting the following [1-9]\d*?)(?P[smhdwMy])?$`)
-// Hour is an int based representation of the time unit.
-const Hour = time.Minute * 60
-
-// Day is an int based representation of the time unit.
-const Day = Hour * 24
-
-// Week is an int based representation of the time unit.
-const Week = Day * 7
-
-// Year is an int based representation of the time unit.
-const Year = Day * 365
-
-// Month is an int based representation of the time unit.
-const Month = Year / 12
-
-const windows = "windows"
-
-// RFC3339Zero is the default value for time.Time.Unix().
-const RFC3339Zero = int64(-62135596800)
-
-const testStringInput = "abcdefghijkl"
-
// AlphaNumericCharacters are literally just valid alphanumeric chars.
var AlphaNumericCharacters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
// ErrTLSVersionNotSupported returned when an unknown TLS version supplied.
var ErrTLSVersionNotSupported = errors.New("supplied TLS version isn't supported")
-
-// TLS13 is the textual representation of TLS 1.3.
-const TLS13 = "1.3"
-
-// TLS12 is the textual representation of TLS 1.2.
-const TLS12 = "1.2"
-
-// TLS11 is the textual representation of TLS 1.1.
-const TLS11 = "1.1"
-
-// TLS10 is the textual representation of TLS 1.0.
-const TLS10 = "1.0"
diff --git a/internal/utils/files.go b/internal/utils/files.go
index a70d11c0..6a14f091 100644
--- a/internal/utils/files.go
+++ b/internal/utils/files.go
@@ -1,12 +1,49 @@
package utils
import (
+ "errors"
"os"
)
-// FileExists returns whether the given file or directory exists.
-func FileExists(path string) (bool, error) {
- _, err := os.Stat(path)
+// FileExists returns true if the given path exists and is a file.
+func FileExists(path string) (exists bool, err error) {
+ info, err := os.Stat(path)
+ if err == nil {
+ if info.IsDir() {
+ return false, errors.New("path is a directory")
+ }
+
+ return true, nil
+ }
+
+ if os.IsNotExist(err) {
+ return false, nil
+ }
+
+ return false, err
+}
+
+// DirectoryExists returns true if the given path exists and is a directory.
+func DirectoryExists(path string) (exists bool, err error) {
+ info, err := os.Stat(path)
+ if err == nil {
+ if info.IsDir() {
+ return true, nil
+ }
+
+ return false, errors.New("path is a file")
+ }
+
+ if os.IsNotExist(err) {
+ return false, nil
+ }
+
+ return false, err
+}
+
+// PathExists returns true if the given path exists.
+func PathExists(path string) (exists bool, err error) {
+ _, err = os.Stat(path)
if err == nil {
return true, nil
}
diff --git a/internal/utils/files_test.go b/internal/utils/files_test.go
new file mode 100644
index 00000000..73b313d8
--- /dev/null
+++ b/internal/utils/files_test.go
@@ -0,0 +1,56 @@
+package utils
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestShouldCheckIfFileExists(t *testing.T) {
+ exists, err := FileExists("../../README.md")
+
+ assert.NoError(t, err)
+ assert.True(t, exists)
+
+ exists, err = FileExists("../../")
+ assert.EqualError(t, err, "path is a directory")
+ assert.False(t, exists)
+
+ exists, err = FileExists("../../NOTAFILE.md")
+ assert.NoError(t, err)
+ assert.False(t, exists)
+}
+
+func TestShouldCheckIfDirectoryExists(t *testing.T) {
+ exists, err := DirectoryExists("../../")
+
+ assert.NoError(t, err)
+ assert.True(t, exists)
+
+ exists, err = DirectoryExists("../../README.md")
+ assert.EqualError(t, err, "path is a file")
+ assert.False(t, exists)
+
+ exists, err = DirectoryExists("../../NOTADIRECTORY/")
+ assert.NoError(t, err)
+ assert.False(t, exists)
+}
+
+func TestShouldCheckIfPathExists(t *testing.T) {
+ exists, err := PathExists("../../README.md")
+
+ assert.NoError(t, err)
+ assert.True(t, exists)
+
+ exists, err = PathExists("../../")
+ assert.NoError(t, err)
+ assert.True(t, exists)
+
+ exists, err = PathExists("../../NOTAFILE.md")
+ assert.NoError(t, err)
+ assert.False(t, exists)
+
+ exists, err = PathExists("../../NOTADIRECTORY/")
+ assert.NoError(t, err)
+ assert.False(t, exists)
+}
diff --git a/internal/utils/hashing.go b/internal/utils/hashing.go
new file mode 100644
index 00000000..16a670bc
--- /dev/null
+++ b/internal/utils/hashing.go
@@ -0,0 +1,13 @@
+package utils
+
+import (
+ "crypto/sha256"
+ "fmt"
+)
+
+// HashSHA256FromString takes an input string and calculates the SHA256 checksum returning it as a base16 hash string.
+func HashSHA256FromString(input string) (output string) {
+ sum := sha256.Sum256([]byte(input))
+
+ return fmt.Sprintf("%x", sum)
+}
diff --git a/internal/utils/rsa.go b/internal/utils/rsa.go
new file mode 100644
index 00000000..cc5a09d6
--- /dev/null
+++ b/internal/utils/rsa.go
@@ -0,0 +1,83 @@
+package utils
+
+import (
+ "crypto/rand"
+ "crypto/rsa"
+ "crypto/x509"
+ "encoding/pem"
+ "errors"
+)
+
+// GenerateRsaKeyPair generate an RSA key pair.
+// bits can be 2048 or 4096.
+func GenerateRsaKeyPair(bits int) (*rsa.PrivateKey, *rsa.PublicKey) {
+ privkey, _ := rsa.GenerateKey(rand.Reader, bits)
+ return privkey, &privkey.PublicKey
+}
+
+// ExportRsaPrivateKeyAsPemStr marshal a rsa private key into PEM string.
+func ExportRsaPrivateKeyAsPemStr(privkey *rsa.PrivateKey) string {
+ privkeyBytes := x509.MarshalPKCS1PrivateKey(privkey)
+ privkeyPem := pem.EncodeToMemory(
+ &pem.Block{
+ Type: "RSA PRIVATE KEY",
+ Bytes: privkeyBytes,
+ },
+ )
+
+ return string(privkeyPem)
+}
+
+// ParseRsaPrivateKeyFromPemStr parse a RSA private key from PEM string.
+func ParseRsaPrivateKeyFromPemStr(privPEM string) (*rsa.PrivateKey, error) {
+ block, _ := pem.Decode([]byte(privPEM))
+ if block == nil {
+ return nil, errors.New("failed to parse PEM block containing the key")
+ }
+
+ priv, err := x509.ParsePKCS1PrivateKey(block.Bytes)
+ if err != nil {
+ return nil, err
+ }
+
+ return priv, nil
+}
+
+// ExportRsaPublicKeyAsPemStr marshal a RSA public into a PEM string.
+func ExportRsaPublicKeyAsPemStr(pubkey *rsa.PublicKey) (string, error) {
+ pubkeyBytes, err := x509.MarshalPKIXPublicKey(pubkey)
+ if err != nil {
+ return "", err
+ }
+
+ pubkeyPem := pem.EncodeToMemory(
+ &pem.Block{
+ Type: "RSA PUBLIC KEY",
+ Bytes: pubkeyBytes,
+ },
+ )
+
+ return string(pubkeyPem), nil
+}
+
+// ParseRsaPublicKeyFromPemStr parse RSA public key from a PEM string.
+func ParseRsaPublicKeyFromPemStr(pubPEM string) (*rsa.PublicKey, error) {
+ block, _ := pem.Decode([]byte(pubPEM))
+ if block == nil {
+ return nil, errors.New("failed to parse PEM block containing the key")
+ }
+
+ pub, err := x509.ParsePKIXPublicKey(block.Bytes)
+ if err != nil {
+ return nil, err
+ }
+
+ switch pub := pub.(type) {
+ case *rsa.PublicKey:
+ return pub, nil
+ default:
+ break // fall through
+ }
+
+ return nil, errors.New("Key type is not RSA")
+}
diff --git a/internal/utils/strings.go b/internal/utils/strings.go
index a1e07e53..56676f3b 100644
--- a/internal/utils/strings.go
+++ b/internal/utils/strings.go
@@ -68,15 +68,13 @@ func SliceString(s string, d int) (array []string) {
return
}
-// IsStringSlicesDifferent checks two slices of strings and on the first occurrence of a string item not existing in the
-// other slice returns true, otherwise returns false.
-func IsStringSlicesDifferent(a, b []string) (different bool) {
+func isStringSlicesDifferent(a, b []string, method func(s string, b []string) bool) (different bool) {
if len(a) != len(b) {
return true
}
for _, s := range a {
- if !IsStringInSlice(s, b) {
+ if !method(s, b) {
return true
}
}
@@ -84,6 +82,18 @@ func IsStringSlicesDifferent(a, b []string) (different bool) {
return false
}
+// IsStringSlicesDifferent checks two slices of strings and on the first occurrence of a string item not existing in the
+// other slice returns true, otherwise returns false.
+func IsStringSlicesDifferent(a, b []string) (different bool) {
+ return isStringSlicesDifferent(a, b, IsStringInSlice)
+}
+
+// IsStringSlicesDifferentFold checks two slices of strings and on the first occurrence of a string item not existing in
+// the other slice (case insensitive) returns true, otherwise returns false.
+func IsStringSlicesDifferentFold(a, b []string) (different bool) {
+ return isStringSlicesDifferent(a, b, IsStringInSliceFold)
+}
+
// StringSlicesDelta takes a before and after []string and compares them returning a added and removed []string.
func StringSlicesDelta(before, after []string) (added, removed []string) {
for _, s := range before {
diff --git a/internal/utils/strings_test.go b/internal/utils/strings_test.go
index 793d42c9..a774ed52 100644
--- a/internal/utils/strings_test.go
+++ b/internal/utils/strings_test.go
@@ -7,6 +7,12 @@ import (
"github.com/stretchr/testify/require"
)
+func TestShouldDetectAlphaNumericString(t *testing.T) {
+ assert.True(t, IsStringAlphaNumeric("abc"))
+ assert.True(t, IsStringAlphaNumeric("abc123"))
+ assert.False(t, IsStringAlphaNumeric("abc123@"))
+}
+
func TestShouldSplitIntoEvenStringsOfFour(t *testing.T) {
input := testStringInput
@@ -70,6 +76,12 @@ func TestShouldFindSliceDifferences(t *testing.T) {
b := []string{"abc", "xyz"}
assert.True(t, IsStringSlicesDifferent(a, b))
+ assert.True(t, IsStringSlicesDifferentFold(a, b))
+
+ c := []string{"Abc", "xyz"}
+
+ assert.True(t, IsStringSlicesDifferent(b, c))
+ assert.False(t, IsStringSlicesDifferentFold(b, c))
}
func TestShouldNotFindSliceDifferences(t *testing.T) {
@@ -77,6 +89,7 @@ func TestShouldNotFindSliceDifferences(t *testing.T) {
b := []string{"abc", "onetwothree"}
assert.False(t, IsStringSlicesDifferent(a, b))
+ assert.False(t, IsStringSlicesDifferentFold(a, b))
}
func TestShouldFindSliceDifferenceWhenDifferentLength(t *testing.T) {
@@ -84,6 +97,7 @@ func TestShouldFindSliceDifferenceWhenDifferentLength(t *testing.T) {
b := []string{"abc", "onetwothree", "more"}
assert.True(t, IsStringSlicesDifferent(a, b))
+ assert.True(t, IsStringSlicesDifferentFold(a, b))
}
func TestShouldFindStringInSliceContains(t *testing.T) {
diff --git a/web/src/App.tsx b/web/src/App.tsx
index c7a70b33..48402a15 100644
--- a/web/src/App.tsx
+++ b/web/src/App.tsx
@@ -14,12 +14,14 @@ import {
RegisterSecurityKeyRoute,
RegisterOneTimePasswordRoute,
LogoutRoute,
+ ConsentRoute,
} from "./Routes";
import * as themes from "./themes";
import { getBasePath } from "./utils/BasePath";
import { getRememberMe, getResetPassword, getTheme } from "./utils/Configuration";
import RegisterOneTimePassword from "./views/DeviceRegistration/RegisterOneTimePassword";
import RegisterSecurityKey from "./views/DeviceRegistration/RegisterSecurityKey";
+import ConsentView from "./views/LoginPortal/ConsentView/ConsentView";
import LoginPortal from "./views/LoginPortal/LoginPortal";
import SignOut from "./views/LoginPortal/SignOut/SignOut";
import ResetPasswordStep1 from "./views/ResetPassword/ResetPasswordStep1";
@@ -65,6 +67,9 @@ const App: React.FC = () => {
+
+
+
diff --git a/web/src/Routes.ts b/web/src/Routes.ts
index 3e3ee0ac..670580f7 100644
--- a/web/src/Routes.ts
+++ b/web/src/Routes.ts
@@ -1,13 +1,14 @@
-export const FirstFactorRoute = "/";
-export const AuthenticatedRoute = "/authenticated";
+export const FirstFactorRoute: string = "/";
+export const AuthenticatedRoute: string = "/authenticated";
+export const ConsentRoute: string = "/consent";
-export const SecondFactorRoute = "/2fa";
-export const SecondFactorU2FRoute = "/2fa/security-key";
-export const SecondFactorTOTPRoute = "/2fa/one-time-password";
-export const SecondFactorPushRoute = "/2fa/push-notification";
+export const SecondFactorRoute: string = "/2fa";
+export const SecondFactorU2FRoute: string = "/2fa/security-key";
+export const SecondFactorTOTPRoute: string = "/2fa/one-time-password";
+export const SecondFactorPushRoute: string = "/2fa/push-notification";
-export const ResetPasswordStep1Route = "/reset-password/step1";
-export const ResetPasswordStep2Route = "/reset-password/step2";
-export const RegisterSecurityKeyRoute = "/security-key/register";
-export const RegisterOneTimePasswordRoute = "/one-time-password/register";
-export const LogoutRoute = "/logout";
+export const ResetPasswordStep1Route: string = "/reset-password/step1";
+export const ResetPasswordStep2Route: string = "/reset-password/step2";
+export const RegisterSecurityKeyRoute: string = "/security-key/register";
+export const RegisterOneTimePasswordRoute: string = "/one-time-password/register";
+export const LogoutRoute: string = "/logout";
diff --git a/web/src/components/NotificationBar.tsx b/web/src/components/NotificationBar.tsx
index f4202bf4..b58e2c90 100644
--- a/web/src/components/NotificationBar.tsx
+++ b/web/src/components/NotificationBar.tsx
@@ -18,7 +18,7 @@ const NotificationBar = function (props: Props) {
if (notification && notification !== null) {
setTmpNotification(notification);
}
- }, [notification]);
+ }, [notification, setTmpNotification]);
const shouldSnackbarBeOpen = notification !== undefined && notification !== null;
diff --git a/web/src/hooks/Consent.ts b/web/src/hooks/Consent.ts
new file mode 100644
index 00000000..9a7023b2
--- /dev/null
+++ b/web/src/hooks/Consent.ts
@@ -0,0 +1,6 @@
+import { getRequestedScopes } from "../services/Consent";
+import { useRemoteCall } from "./RemoteCall";
+
+export function useRequestedScopes() {
+ return useRemoteCall(getRequestedScopes, []);
+}
diff --git a/web/src/hooks/Redirector.ts b/web/src/hooks/Redirector.ts
new file mode 100644
index 00000000..18197a12
--- /dev/null
+++ b/web/src/hooks/Redirector.ts
@@ -0,0 +1,5 @@
+export function useRedirector() {
+ return (url: string) => {
+ window.location.href = url;
+ };
+}
diff --git a/web/src/hooks/RemoteCall.ts b/web/src/hooks/RemoteCall.ts
index a33afb37..be26ae18 100644
--- a/web/src/hooks/RemoteCall.ts
+++ b/web/src/hooks/RemoteCall.ts
@@ -5,23 +5,25 @@ type PromisifiedFunction = (...args: any) => Promise;
export function useRemoteCall(
fn: PromisifiedFunction,
deps: DependencyList,
-): [Ret | undefined, PromisifiedFunction, boolean, Error | undefined] {
+): [Ret | undefined, () => void, boolean, Error | undefined] {
const [data, setData] = useState(undefined as Ret | undefined);
const [inProgress, setInProgress] = useState(false);
const [error, setError] = useState(undefined as Error | undefined);
const fnCallback = useCallback(fn, [fn, deps]);
- const triggerCallback = useCallback(async () => {
- try {
- setInProgress(true);
- const res = await fnCallback();
- setInProgress(false);
- setData(res);
- } catch (err) {
- console.error(err);
- setError(err);
- }
+ const triggerCallback = useCallback(() => {
+ (async () => {
+ try {
+ setInProgress(true);
+ const res = await fnCallback();
+ setInProgress(false);
+ setData(res);
+ } catch (err) {
+ console.error(err);
+ setError(err);
+ }
+ })();
}, [setInProgress, setError, fnCallback]);
return [data, triggerCallback, inProgress, error];
diff --git a/web/src/layouts/LoginLayout.tsx b/web/src/layouts/LoginLayout.tsx
index 858abc8d..81a6106d 100644
--- a/web/src/layouts/LoginLayout.tsx
+++ b/web/src/layouts/LoginLayout.tsx
@@ -8,7 +8,7 @@ import { ReactComponent as UserSvg } from "../assets/images/user.svg";
export interface Props {
id?: string;
children?: ReactNode;
- title: string;
+ title?: string;
showBrand?: boolean;
}
@@ -21,11 +21,13 @@ const LoginLayout = function (props: Props) {
-
-
- {props.title}
-
-
+ {props.title ? (
+
+
+ {props.title}
+
+
+ ) : null}
{props.children}
@@ -63,7 +65,11 @@ const useStyles = makeStyles((theme) => ({
width: "64px",
fill: theme.custom.icon,
},
- body: {},
+ body: {
+ marginTop: theme.spacing(),
+ paddingTop: theme.spacing(),
+ paddingBottom: theme.spacing(),
+ },
poweredBy: {
fontSize: "0.7em",
color: grey[500],
diff --git a/web/src/services/Api.ts b/web/src/services/Api.ts
index 2b5be2ce..a298723d 100644
--- a/web/src/services/Api.ts
+++ b/web/src/services/Api.ts
@@ -4,6 +4,9 @@ import { getBasePath } from "../utils/BasePath";
const basePath = getBasePath();
+// Note: If you change this const you must also do so in the backend at internal/handlers/cost.go.
+export const ConsentPath = basePath + "/api/oidc/consent";
+
export const FirstFactorPath = basePath + "/api/firstfactor";
export const InitiateTOTPRegistrationPath = basePath + "/api/secondfactor/totp/identity/start";
export const CompleteTOTPRegistrationPath = basePath + "/api/secondfactor/totp/identity/finish";
diff --git a/web/src/services/Consent.ts b/web/src/services/Consent.ts
new file mode 100644
index 00000000..da38e11c
--- /dev/null
+++ b/web/src/services/Consent.ts
@@ -0,0 +1,42 @@
+import { ConsentPath } from "./Api";
+import { Post, Get } from "./Client";
+
+interface ConsentPostRequestBody {
+ client_id: string;
+ accept_or_reject: "accept" | "reject";
+}
+
+interface ConsentPostResponseBody {
+ redirect_uri: string;
+}
+
+interface ConsentGetResponseBody {
+ client_id: string;
+ client_description: string;
+ scopes: Scope[];
+ audience: Audience[];
+}
+
+interface Scope {
+ name: string;
+ description: string;
+}
+
+interface Audience {
+ name: string;
+ description: string;
+}
+
+export function getRequestedScopes() {
+ return Get(ConsentPath);
+}
+
+export function acceptConsent(clientID: string) {
+ const body: ConsentPostRequestBody = { client_id: clientID, accept_or_reject: "accept" };
+ return Post(ConsentPath, body);
+}
+
+export function rejectConsent(clientID: string) {
+ const body: ConsentPostRequestBody = { client_id: clientID, accept_or_reject: "reject" };
+ return Post(ConsentPath, body);
+}
diff --git a/web/src/views/LoginPortal/ConsentView/ConsentView.tsx b/web/src/views/LoginPortal/ConsentView/ConsentView.tsx
new file mode 100644
index 00000000..62437b25
--- /dev/null
+++ b/web/src/views/LoginPortal/ConsentView/ConsentView.tsx
@@ -0,0 +1,187 @@
+import React, { useEffect, Fragment, ReactNode } from "react";
+
+import { Button, Grid, List, ListItem, ListItemIcon, ListItemText, Tooltip, makeStyles } from "@material-ui/core";
+import { AccountBox, CheckBox, Contacts, Drafts, Group } from "@material-ui/icons";
+import { useHistory } from "react-router-dom";
+
+import { useRequestedScopes } from "../../../hooks/Consent";
+import { useNotifications } from "../../../hooks/NotificationsContext";
+import { useRedirector } from "../../../hooks/Redirector";
+import LoginLayout from "../../../layouts/LoginLayout";
+import { acceptConsent, rejectConsent } from "../../../services/Consent";
+import LoadingPage from "../../LoadingPage/LoadingPage";
+
+export interface Props {}
+
+function showListItemAvatar(id: string) {
+ switch (id) {
+ case "openid":
+ return ;
+ case "profile":
+ return ;
+ case "groups":
+ return ;
+ case "email":
+ return ;
+ default:
+ return ;
+ }
+}
+
+const ConsentView = function (props: Props) {
+ const classes = useStyles();
+ const history = useHistory();
+ const redirect = useRedirector();
+ const { createErrorNotification, resetNotification } = useNotifications();
+ const [resp, fetch, , err] = useRequestedScopes();
+
+ useEffect(() => {
+ if (err) {
+ history.replace("/");
+ console.error(`Unable to display consent screen: ${err.message}`);
+ }
+ }, [history, resetNotification, createErrorNotification, err]);
+
+ useEffect(() => {
+ fetch();
+ }, [fetch]);
+
+ const handleAcceptConsent = async () => {
+ // This case should not happen in theory because the buttons are disabled when response is undefined.
+ if (!resp) {
+ return;
+ }
+ const res = await acceptConsent(resp.client_id);
+ if (res.redirect_uri) {
+ redirect(res.redirect_uri);
+ } else {
+ throw new Error("Unable to redirect the user");
+ }
+ };
+
+ const handleRejectConsent = async () => {
+ if (!resp) {
+ return;
+ }
+ const res = await rejectConsent(resp.client_id);
+ if (res.redirect_uri) {
+ redirect(res.redirect_uri);
+ } else {
+ throw new Error("Unable to redirect the user");
+ }
+ };
+
+ return (
+
+
+
+
+
+ The application
+ {` ${resp?.client_description} (${resp?.client_id}) `}
+ is requesting the following permissions
+
+
+
+
+
+ {resp?.scopes.map((s) => (
+
+
+ {showListItemAvatar(s.name)}
+
+
+
+ ))}
+
+
+
+
+
+
+
+ Accept
+
+
+
+
+ Deny
+
+
+
+
+
+
+
+ );
+};
+
+const useStyles = makeStyles((theme) => ({
+ container: {
+ paddingTop: theme.spacing(4),
+ paddingBottom: theme.spacing(4),
+ display: "block",
+ justifyContent: "center",
+ },
+ scopesListContainer: {
+ textAlign: "center",
+ },
+ scopesList: {
+ display: "inline-block",
+ backgroundColor: theme.palette.background.paper,
+ marginTop: theme.spacing(2),
+ marginBottom: theme.spacing(2),
+ },
+ clientID: {
+ fontWeight: "bold",
+ },
+ button: {
+ marginLeft: theme.spacing(),
+ marginRight: theme.spacing(),
+ width: "100%",
+ },
+ bulletIcon: {
+ display: "inline-block",
+ },
+ permissionsContainer: {
+ border: "1px solid #dedede",
+ margin: theme.spacing(4),
+ },
+ listItem: {
+ textAlign: "center",
+ marginRight: theme.spacing(2),
+ },
+}));
+
+export default ConsentView;
+
+interface ComponentOrLoadingProps {
+ ready: boolean;
+
+ children: ReactNode;
+}
+
+function ComponentOrLoading(props: ComponentOrLoadingProps) {
+ return (
+
+
+
+
+ {props.ready ? props.children : null}
+
+ );
+}
diff --git a/web/src/views/LoginPortal/FirstFactor/FirstFactorForm.tsx b/web/src/views/LoginPortal/FirstFactor/FirstFactorForm.tsx
index a9e8b900..346310ac 100644
--- a/web/src/views/LoginPortal/FirstFactor/FirstFactorForm.tsx
+++ b/web/src/views/LoginPortal/FirstFactor/FirstFactorForm.tsx
@@ -79,7 +79,7 @@ const FirstFactorForm = function (props: Props) {
return (
-
+
({
- root: {
- marginTop: theme.spacing(),
- marginBottom: theme.spacing(),
- },
actionRow: {
display: "flex",
flexDirection: "row",
diff --git a/web/src/views/LoginPortal/LoginPortal.tsx b/web/src/views/LoginPortal/LoginPortal.tsx
index 9601f8f2..a53cf8f2 100644
--- a/web/src/views/LoginPortal/LoginPortal.tsx
+++ b/web/src/views/LoginPortal/LoginPortal.tsx
@@ -5,6 +5,7 @@ import { Switch, Route, Redirect, useHistory, useLocation } from "react-router";
import { useConfiguration } from "../../hooks/Configuration";
import { useNotifications } from "../../hooks/NotificationsContext";
import { useRedirectionURL } from "../../hooks/RedirectionURL";
+import { useRedirector } from "../../hooks/Redirector";
import { useRequestMethod } from "../../hooks/RequestMethod";
import { useAutheliaState } from "../../hooks/State";
import { useUserPreferences as userUserInfo } from "../../hooks/UserInfo";
@@ -35,6 +36,7 @@ const LoginPortal = function (props: Props) {
const requestMethod = useRequestMethod();
const { createErrorNotification } = useNotifications();
const [firstFactorDisabled, setFirstFactorDisabled] = useState(true);
+ const redirector = useRedirector();
const [state, fetchState, , fetchStateError] = useAutheliaState();
const [userInfo, fetchUserInfo, , fetchUserInfoError] = userUserInfo();
@@ -112,7 +114,7 @@ const LoginPortal = function (props: Props) {
const handleAuthSuccess = async (redirectionURL: string | undefined) => {
if (redirectionURL) {
// Do an external redirection pushed by the server.
- window.location.href = redirectionURL;
+ redirector(redirectionURL);
} else {
// Refresh state
fetchState();
@@ -152,6 +154,7 @@ const LoginPortal = function (props: Props) {
{userInfo ? : null}
+ {/* By default we route to first factor page */}
diff --git a/web/src/views/LoginPortal/SignOut/SignOut.tsx b/web/src/views/LoginPortal/SignOut/SignOut.tsx
index c1a95794..dd9e6b6b 100644
--- a/web/src/views/LoginPortal/SignOut/SignOut.tsx
+++ b/web/src/views/LoginPortal/SignOut/SignOut.tsx
@@ -6,6 +6,7 @@ import { Redirect } from "react-router";
import { useIsMountedRef } from "../../../hooks/Mounted";
import { useNotifications } from "../../../hooks/NotificationsContext";
import { useRedirectionURL } from "../../../hooks/RedirectionURL";
+import { useRedirector } from "../../../hooks/Redirector";
import LoginLayout from "../../../layouts/LoginLayout";
import { FirstFactorRoute } from "../../../Routes";
import { signOut } from "../../../services/SignOut";
@@ -17,6 +18,7 @@ const SignOut = function (props: Props) {
const style = useStyles();
const { createErrorNotification } = useNotifications();
const redirectionURL = useRedirectionURL();
+ const redirector = useRedirector();
const [timedOut, setTimedOut] = useState(false);
const [safeRedirect, setSafeRedirect] = useState(false);
@@ -44,7 +46,7 @@ const SignOut = function (props: Props) {
if (timedOut) {
if (redirectionURL && safeRedirect) {
- window.location.href = redirectionURL;
+ redirector(redirectionURL);
} else {
return ;
}