Merge pull request #330 from zitadel/main-next
Merges the next branch into main, releasing V2.
This commit is contained in:
commit
c3775aceaa
122 changed files with 8195 additions and 2858 deletions
4
.github/workflows/codeql-analysis.yml
vendored
4
.github/workflows/codeql-analysis.yml
vendored
|
@ -2,10 +2,10 @@ name: "Code scanning - action"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main, ]
|
branches: [main,next]
|
||||||
pull_request:
|
pull_request:
|
||||||
# The branches below must be a subset of the branches above
|
# The branches below must be a subset of the branches above
|
||||||
branches: [main]
|
branches: [main,next]
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '0 11 * * 0'
|
- cron: '0 11 * * 0'
|
||||||
|
|
||||||
|
|
7
.github/workflows/release.yml
vendored
7
.github/workflows/release.yml
vendored
|
@ -3,6 +3,7 @@ on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
- next
|
||||||
tags-ignore:
|
tags-ignore:
|
||||||
- '**'
|
- '**'
|
||||||
pull_request:
|
pull_request:
|
||||||
|
@ -15,7 +16,7 @@ jobs:
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
go: ['1.16', '1.17', '1.18', '1.19', '1.20']
|
go: ['1.18', '1.19', '1.20']
|
||||||
name: Go ${{ matrix.go }} test
|
name: Go ${{ matrix.go }} test
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
@ -23,7 +24,7 @@ jobs:
|
||||||
uses: actions/setup-go@v3
|
uses: actions/setup-go@v3
|
||||||
with:
|
with:
|
||||||
go-version: ${{ matrix.go }}
|
go-version: ${{ matrix.go }}
|
||||||
- run: go test -race -v -coverprofile=profile.cov -coverpkg=github.com/zitadel/oidc/... ./pkg/...
|
- run: go test -race -v -coverprofile=profile.cov -coverpkg=./pkg/... ./pkg/...
|
||||||
- uses: codecov/codecov-action@v3.1.1
|
- uses: codecov/codecov-action@v3.1.1
|
||||||
with:
|
with:
|
||||||
file: ./profile.cov
|
file: ./profile.cov
|
||||||
|
@ -31,7 +32,7 @@ jobs:
|
||||||
release:
|
release:
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
needs: [test]
|
needs: [test]
|
||||||
if: ${{ github.event_name == 'workflow_dispatch' || github.ref == 'refs/heads/main' }}
|
if: ${{ github.event_name == 'workflow_dispatch' || github.ref == 'refs/heads/main' || github.ref == 'refs/heads/next' }}
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
steps:
|
steps:
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
branches: ["main"],
|
branches: [
|
||||||
|
{name: "main"},
|
||||||
|
{name: "next", prerelease: true},
|
||||||
|
],
|
||||||
plugins: [
|
plugins: [
|
||||||
"@semantic-release/commit-analyzer",
|
"@semantic-release/commit-analyzer",
|
||||||
"@semantic-release/release-notes-generator",
|
"@semantic-release/release-notes-generator",
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
|
|
||||||
# Backwards-incompatible changes to be made in the next major release
|
|
||||||
|
|
||||||
- Add `rp/RelyingParty.GetRevokeEndpoint`
|
|
||||||
- Rename `op/OpStorage.GetKeyByIDAndUserID` to `op/OpStorage.GetKeyByIDAndClientID`
|
|
||||||
- Add `CanRefreshTokenInfo` (`GetRefreshTokenInfo()`) to `op.Storage`
|
|
||||||
|
|
23
README.md
23
README.md
|
@ -34,7 +34,7 @@ The most important packages of the library:
|
||||||
/client/app web app / RP demonstrating authorization code flow using various authentication methods (code, PKCE, JWT profile)
|
/client/app web app / RP demonstrating authorization code flow using various authentication methods (code, PKCE, JWT profile)
|
||||||
/client/github example of the extended OAuth2 library, providing an HTTP client with a reuse token source
|
/client/github example of the extended OAuth2 library, providing an HTTP client with a reuse token source
|
||||||
/client/service demonstration of JWT Profile Authorization Grant
|
/client/service demonstration of JWT Profile Authorization Grant
|
||||||
/server example of an OpenID Provider implementation including some very basic login UI
|
/server examples of an OpenID Provider implementations (including dynamic) with some very basic login UI
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
## How To Use It
|
## How To Use It
|
||||||
|
@ -44,16 +44,27 @@ Check the `/example` folder where example code for different scenarios is locate
|
||||||
```bash
|
```bash
|
||||||
# start oidc op server
|
# start oidc op server
|
||||||
# oidc discovery http://localhost:9998/.well-known/openid-configuration
|
# oidc discovery http://localhost:9998/.well-known/openid-configuration
|
||||||
go run github.com/zitadel/oidc/example/server
|
go run github.com/zitadel/oidc/v2/example/server
|
||||||
# start oidc web client (in a new terminal)
|
# start oidc web client (in a new terminal)
|
||||||
CLIENT_ID=web CLIENT_SECRET=secret ISSUER=http://localhost:9998 SCOPES="openid profile" PORT=9999 go run github.com/zitadel/oidc/example/client/app
|
CLIENT_ID=web CLIENT_SECRET=secret ISSUER=http://localhost:9998/ SCOPES="openid profile" PORT=9999 go run github.com/zitadel/oidc/v2/example/client/app
|
||||||
```
|
```
|
||||||
|
|
||||||
- open http://localhost:9999/login in your browser
|
- open http://localhost:9999/login in your browser
|
||||||
- you will be redirected to op server and the login UI
|
- you will be redirected to op server and the login UI
|
||||||
- login with user `test-user` and password `verysecure`
|
- login with user `test-user@localhost` and password `verysecure`
|
||||||
- the OP will redirect you to the client app, which displays the user info
|
- the OP will redirect you to the client app, which displays the user info
|
||||||
|
|
||||||
|
for the dynamic issuer, just start it with:
|
||||||
|
```bash
|
||||||
|
go run github.com/zitadel/oidc/v2/example/server/dynamic
|
||||||
|
```
|
||||||
|
the oidc web client above will still work, but if you add `oidc.local` (pointing to 127.0.0.1) in your hosts file you can also start it with:
|
||||||
|
```bash
|
||||||
|
CLIENT_ID=web CLIENT_SECRET=secret ISSUER=http://oidc.local:9998/ SCOPES="openid profile" PORT=9999 go run github.com/zitadel/oidc/v2/example/client/app
|
||||||
|
```
|
||||||
|
|
||||||
|
> Note: Usernames are suffixed with the hostname (`test-user@localhost` or `test-user@oidc.local`)
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
| | Code Flow | Implicit Flow | Hybrid Flow | Discovery | PKCE | Token Exchange | mTLS | JWT Profile | Refresh Token | Client Credentials |
|
| | Code Flow | Implicit Flow | Hybrid Flow | Discovery | PKCE | Token Exchange | mTLS | JWT Profile | Refresh Token | Client Credentials |
|
||||||
|
@ -87,9 +98,7 @@ Versions that also build are marked with :warning:.
|
||||||
|
|
||||||
| Version | Supported |
|
| Version | Supported |
|
||||||
|---------|--------------------|
|
|---------|--------------------|
|
||||||
| <1.16 | :x: |
|
| <1.18 | :x: |
|
||||||
| 1.16 | :warning: |
|
|
||||||
| 1.17 | :warning: |
|
|
||||||
| 1.18 | :warning: |
|
| 1.18 | :warning: |
|
||||||
| 1.19 | :white_check_mark: |
|
| 1.19 | :white_check_mark: |
|
||||||
| 1.20 | :white_check_mark: |
|
| 1.20 | :white_check_mark: |
|
||||||
|
|
|
@ -12,8 +12,8 @@ import (
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
|
||||||
"github.com/zitadel/oidc/pkg/client/rs"
|
"github.com/zitadel/oidc/v2/pkg/client/rs"
|
||||||
"github.com/zitadel/oidc/pkg/oidc"
|
"github.com/zitadel/oidc/v2/pkg/oidc"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -76,7 +76,7 @@ func main() {
|
||||||
params := mux.Vars(r)
|
params := mux.Vars(r)
|
||||||
requestedClaim := params["claim"]
|
requestedClaim := params["claim"]
|
||||||
requestedValue := params["value"]
|
requestedValue := params["value"]
|
||||||
value, ok := resp.GetClaim(requestedClaim).(string)
|
value, ok := resp.Claims[requestedClaim].(string)
|
||||||
if !ok || value == "" || value != requestedValue {
|
if !ok || value == "" || value != requestedValue {
|
||||||
http.Error(w, "claim does not match", http.StatusForbidden)
|
http.Error(w, "claim does not match", http.StatusForbidden)
|
||||||
return
|
return
|
||||||
|
|
|
@ -11,9 +11,9 @@ import (
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
|
||||||
"github.com/zitadel/oidc/pkg/client/rp"
|
"github.com/zitadel/oidc/v2/pkg/client/rp"
|
||||||
httphelper "github.com/zitadel/oidc/pkg/http"
|
httphelper "github.com/zitadel/oidc/v2/pkg/http"
|
||||||
"github.com/zitadel/oidc/pkg/oidc"
|
"github.com/zitadel/oidc/v2/pkg/oidc"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -62,7 +62,7 @@ func main() {
|
||||||
http.Handle("/login", rp.AuthURLHandler(state, provider, rp.WithPromptURLParam("Welcome back!")))
|
http.Handle("/login", rp.AuthURLHandler(state, provider, rp.WithPromptURLParam("Welcome back!")))
|
||||||
|
|
||||||
// for demonstration purposes the returned userinfo response is written as JSON object onto response
|
// for demonstration purposes the returned userinfo response is written as JSON object onto response
|
||||||
marshalUserinfo := func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens, state string, rp rp.RelyingParty, info oidc.UserInfo) {
|
marshalUserinfo := func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens[*oidc.IDTokenClaims], state string, rp rp.RelyingParty, info *oidc.UserInfo) {
|
||||||
data, err := json.Marshal(info)
|
data, err := json.Marshal(info)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
@ -82,6 +82,31 @@ func main() {
|
||||||
// w.Write(data)
|
// w.Write(data)
|
||||||
//}
|
//}
|
||||||
|
|
||||||
|
// you can also try token exchange flow
|
||||||
|
//
|
||||||
|
// requestTokenExchange := func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens, state string, rp rp.RelyingParty, info oidc.UserInfo) {
|
||||||
|
// data := make(url.Values)
|
||||||
|
// data.Set("grant_type", string(oidc.GrantTypeTokenExchange))
|
||||||
|
// data.Set("requested_token_type", string(oidc.IDTokenType))
|
||||||
|
// data.Set("subject_token", tokens.RefreshToken)
|
||||||
|
// data.Set("subject_token_type", string(oidc.RefreshTokenType))
|
||||||
|
// data.Add("scope", "profile custom_scope:impersonate:id2")
|
||||||
|
|
||||||
|
// client := &http.Client{}
|
||||||
|
// r2, _ := http.NewRequest(http.MethodPost, issuer+"/oauth/token", strings.NewReader(data.Encode()))
|
||||||
|
// // r2.Header.Add("Authorization", "Basic "+"d2ViOnNlY3JldA==")
|
||||||
|
// r2.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
// r2.SetBasicAuth("web", "secret")
|
||||||
|
|
||||||
|
// resp, _ := client.Do(r2)
|
||||||
|
// fmt.Println(resp.Status)
|
||||||
|
|
||||||
|
// b, _ := io.ReadAll(resp.Body)
|
||||||
|
// resp.Body.Close()
|
||||||
|
|
||||||
|
// w.Write(b)
|
||||||
|
// }
|
||||||
|
|
||||||
// register the CodeExchangeHandler at the callbackPath
|
// register the CodeExchangeHandler at the callbackPath
|
||||||
// the CodeExchangeHandler handles the auth response, creates the token request and calls the callback function
|
// the CodeExchangeHandler handles the auth response, creates the token request and calls the callback function
|
||||||
// with the returned tokens from the token endpoint
|
// with the returned tokens from the token endpoint
|
||||||
|
|
61
example/client/device/device.go
Normal file
61
example/client/device/device.go
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"github.com/zitadel/oidc/v2/pkg/client/rp"
|
||||||
|
httphelper "github.com/zitadel/oidc/v2/pkg/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
key = []byte("test1234test1234")
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGINT)
|
||||||
|
defer stop()
|
||||||
|
|
||||||
|
clientID := os.Getenv("CLIENT_ID")
|
||||||
|
clientSecret := os.Getenv("CLIENT_SECRET")
|
||||||
|
keyPath := os.Getenv("KEY_PATH")
|
||||||
|
issuer := os.Getenv("ISSUER")
|
||||||
|
scopes := strings.Split(os.Getenv("SCOPES"), " ")
|
||||||
|
|
||||||
|
cookieHandler := httphelper.NewCookieHandler(key, key, httphelper.WithUnsecure())
|
||||||
|
|
||||||
|
var options []rp.Option
|
||||||
|
if clientSecret == "" {
|
||||||
|
options = append(options, rp.WithPKCE(cookieHandler))
|
||||||
|
}
|
||||||
|
if keyPath != "" {
|
||||||
|
options = append(options, rp.WithJWTProfile(rp.SignerFromKeyPath(keyPath)))
|
||||||
|
}
|
||||||
|
|
||||||
|
provider, err := rp.NewRelyingPartyOIDC(issuer, clientID, clientSecret, "", scopes, options...)
|
||||||
|
if err != nil {
|
||||||
|
logrus.Fatalf("error creating provider %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
logrus.Info("starting device authorization flow")
|
||||||
|
resp, err := rp.DeviceAuthorization(scopes, provider)
|
||||||
|
if err != nil {
|
||||||
|
logrus.Fatal(err)
|
||||||
|
}
|
||||||
|
logrus.Info("resp", resp)
|
||||||
|
fmt.Printf("\nPlease browse to %s and enter code %s\n", resp.VerificationURI, resp.UserCode)
|
||||||
|
|
||||||
|
logrus.Info("start polling")
|
||||||
|
token, err := rp.DeviceAccessToken(ctx, resp.DeviceCode, time.Duration(resp.Interval)*time.Second, provider)
|
||||||
|
if err != nil {
|
||||||
|
logrus.Fatal(err)
|
||||||
|
}
|
||||||
|
logrus.Infof("successfully obtained token: %v", token)
|
||||||
|
}
|
|
@ -10,9 +10,10 @@ import (
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
githubOAuth "golang.org/x/oauth2/github"
|
githubOAuth "golang.org/x/oauth2/github"
|
||||||
|
|
||||||
"github.com/zitadel/oidc/pkg/client/rp"
|
"github.com/zitadel/oidc/v2/pkg/client/rp"
|
||||||
"github.com/zitadel/oidc/pkg/client/rp/cli"
|
"github.com/zitadel/oidc/v2/pkg/client/rp/cli"
|
||||||
"github.com/zitadel/oidc/pkg/http"
|
"github.com/zitadel/oidc/v2/pkg/http"
|
||||||
|
"github.com/zitadel/oidc/v2/pkg/oidc"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -43,7 +44,7 @@ func main() {
|
||||||
state := func() string {
|
state := func() string {
|
||||||
return uuid.New().String()
|
return uuid.New().String()
|
||||||
}
|
}
|
||||||
token := cli.CodeFlow(ctx, relyingParty, callbackPath, port, state)
|
token := cli.CodeFlow[*oidc.IDTokenClaims](ctx, relyingParty, callbackPath, port, state)
|
||||||
|
|
||||||
client := github.NewClient(relyingParty.OAuthConfig().Client(ctx, token.Token))
|
client := github.NewClient(relyingParty.OAuthConfig().Client(ctx, token.Token))
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,7 @@ import (
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
|
|
||||||
"github.com/zitadel/oidc/pkg/client/profile"
|
"github.com/zitadel/oidc/v2/pkg/client/profile"
|
||||||
)
|
)
|
||||||
|
|
||||||
var client = http.DefaultClient
|
var client = http.DefaultClient
|
||||||
|
|
|
@ -5,7 +5,6 @@ Package example contains some example of the various use of this library:
|
||||||
/app web app / RP demonstrating authorization code flow using various authentication methods (code, PKCE, JWT profile)
|
/app web app / RP demonstrating authorization code flow using various authentication methods (code, PKCE, JWT profile)
|
||||||
/github example of the extended OAuth2 library, providing an HTTP client with a reuse token source
|
/github example of the extended OAuth2 library, providing an HTTP client with a reuse token source
|
||||||
/service demonstration of JWT Profile Authorization Grant
|
/service demonstration of JWT Profile Authorization Grant
|
||||||
/server example of an OpenID Provider implementation including some very basic login UI
|
/server examples of an OpenID Provider implementations (including dynamic) with some very basic
|
||||||
|
|
||||||
*/
|
*/
|
||||||
package example
|
package example
|
||||||
|
|
113
example/server/dynamic/login.go
Normal file
113
example/server/dynamic/login.go
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"html/template"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
|
||||||
|
"github.com/zitadel/oidc/v2/pkg/op"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
queryAuthRequestID = "authRequestID"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
loginTmpl, _ = template.New("login").Parse(`
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Login</title>
|
||||||
|
</head>
|
||||||
|
<body style="display: flex; align-items: center; justify-content: center; height: 100vh;">
|
||||||
|
<form method="POST" action="/login/username" style="height: 200px; width: 200px;">
|
||||||
|
<input type="hidden" name="id" value="{{.ID}}">
|
||||||
|
<div>
|
||||||
|
<label for="username">Username:</label>
|
||||||
|
<input id="username" name="username" style="width: 100%">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="password">Password:</label>
|
||||||
|
<input id="password" name="password" style="width: 100%">
|
||||||
|
</div>
|
||||||
|
<p style="color:red; min-height: 1rem;">{{.Error}}</p>
|
||||||
|
<button type="submit">Login</button>
|
||||||
|
</form>
|
||||||
|
</body>
|
||||||
|
</html>`)
|
||||||
|
)
|
||||||
|
|
||||||
|
type login struct {
|
||||||
|
authenticate authenticate
|
||||||
|
router *mux.Router
|
||||||
|
callback func(context.Context, string) string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLogin(authenticate authenticate, callback func(context.Context, string) string, issuerInterceptor *op.IssuerInterceptor) *login {
|
||||||
|
l := &login{
|
||||||
|
authenticate: authenticate,
|
||||||
|
callback: callback,
|
||||||
|
}
|
||||||
|
l.createRouter(issuerInterceptor)
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *login) createRouter(issuerInterceptor *op.IssuerInterceptor) {
|
||||||
|
l.router = mux.NewRouter()
|
||||||
|
l.router.Path("/username").Methods("GET").HandlerFunc(l.loginHandler)
|
||||||
|
l.router.Path("/username").Methods("POST").HandlerFunc(issuerInterceptor.HandlerFunc(l.checkLoginHandler))
|
||||||
|
}
|
||||||
|
|
||||||
|
type authenticate interface {
|
||||||
|
CheckUsernamePassword(ctx context.Context, username, password, id string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *login) loginHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
err := r.ParseForm()
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, fmt.Sprintf("cannot parse form:%s", err), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
//the oidc package will pass the id of the auth request as query parameter
|
||||||
|
//we will use this id through the login process and therefore pass it to the login page
|
||||||
|
renderLogin(w, r.FormValue(queryAuthRequestID), nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderLogin(w http.ResponseWriter, id string, err error) {
|
||||||
|
var errMsg string
|
||||||
|
if err != nil {
|
||||||
|
errMsg = err.Error()
|
||||||
|
}
|
||||||
|
data := &struct {
|
||||||
|
ID string
|
||||||
|
Error string
|
||||||
|
}{
|
||||||
|
ID: id,
|
||||||
|
Error: errMsg,
|
||||||
|
}
|
||||||
|
err = loginTmpl.Execute(w, data)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *login) checkLoginHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
err := r.ParseForm()
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, fmt.Sprintf("cannot parse form:%s", err), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
username := r.FormValue("username")
|
||||||
|
password := r.FormValue("password")
|
||||||
|
id := r.FormValue("id")
|
||||||
|
err = l.authenticate.CheckUsernamePassword(r.Context(), username, password, id)
|
||||||
|
if err != nil {
|
||||||
|
renderLogin(w, id, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
http.Redirect(w, r, l.callback(r.Context(), id), http.StatusFound)
|
||||||
|
}
|
138
example/server/dynamic/op.go
Normal file
138
example/server/dynamic/op.go
Normal file
|
@ -0,0 +1,138 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/sha256"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"golang.org/x/text/language"
|
||||||
|
|
||||||
|
"github.com/zitadel/oidc/v2/example/server/storage"
|
||||||
|
"github.com/zitadel/oidc/v2/pkg/op"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
pathLoggedOut = "/logged-out"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
hostnames = []string{
|
||||||
|
"localhost", //note that calling 127.0.0.1 / ::1 won't work as the hostname does not match
|
||||||
|
"oidc.local", //add this to your hosts file (pointing to 127.0.0.1)
|
||||||
|
//feel free to add more...
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
storage.RegisterClients(
|
||||||
|
storage.NativeClient("native"),
|
||||||
|
storage.WebClient("web", "secret"),
|
||||||
|
storage.WebClient("api", "secret"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
port := "9998"
|
||||||
|
issuers := make([]string, len(hostnames))
|
||||||
|
for i, hostname := range hostnames {
|
||||||
|
issuers[i] = fmt.Sprintf("http://%s:%s/", hostname, port)
|
||||||
|
}
|
||||||
|
|
||||||
|
//the OpenID Provider requires a 32-byte key for (token) encryption
|
||||||
|
//be sure to create a proper crypto random key and manage it securely!
|
||||||
|
key := sha256.Sum256([]byte("test"))
|
||||||
|
|
||||||
|
router := mux.NewRouter()
|
||||||
|
|
||||||
|
//for simplicity, we provide a very small default page for users who have signed out
|
||||||
|
router.HandleFunc(pathLoggedOut, func(w http.ResponseWriter, req *http.Request) {
|
||||||
|
_, err := w.Write([]byte("signed out successfully"))
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error serving logged out page: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
//the OpenIDProvider interface needs a Storage interface handling various checks and state manipulations
|
||||||
|
//this might be the layer for accessing your database
|
||||||
|
//in this example it will be handled in-memory
|
||||||
|
//the NewMultiStorage is able to handle multiple issuers
|
||||||
|
storage := storage.NewMultiStorage(issuers)
|
||||||
|
|
||||||
|
//creation of the OpenIDProvider with the just created in-memory Storage
|
||||||
|
provider, err := newDynamicOP(ctx, storage, key)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
//the provider will only take care of the OpenID Protocol, so there must be some sort of UI for the login process
|
||||||
|
//for the simplicity of the example this means a simple page with username and password field
|
||||||
|
//be sure to provide an IssuerInterceptor with the IssuerFromRequest from the OP so the login can select / and pass it to the storage
|
||||||
|
l := NewLogin(storage, op.AuthCallbackURL(provider), op.NewIssuerInterceptor(provider.IssuerFromRequest))
|
||||||
|
|
||||||
|
//regardless of how many pages / steps there are in the process, the UI must be registered in the router,
|
||||||
|
//so we will direct all calls to /login to the login UI
|
||||||
|
router.PathPrefix("/login/").Handler(http.StripPrefix("/login", l.router))
|
||||||
|
|
||||||
|
//we register the http handler of the OP on the root, so that the discovery endpoint (/.well-known/openid-configuration)
|
||||||
|
//is served on the correct path
|
||||||
|
//
|
||||||
|
//if your issuer ends with a path (e.g. http://localhost:9998/custom/path/),
|
||||||
|
//then you would have to set the path prefix (/custom/path/):
|
||||||
|
//router.PathPrefix("/custom/path/").Handler(http.StripPrefix("/custom/path", provider.HttpHandler()))
|
||||||
|
router.PathPrefix("/").Handler(provider.HttpHandler())
|
||||||
|
|
||||||
|
server := &http.Server{
|
||||||
|
Addr: ":" + port,
|
||||||
|
Handler: router,
|
||||||
|
}
|
||||||
|
err = server.ListenAndServe()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
<-ctx.Done()
|
||||||
|
}
|
||||||
|
|
||||||
|
// newDynamicOP will create an OpenID Provider for localhost on a specified port with a given encryption key
|
||||||
|
// and a predefined default logout uri
|
||||||
|
// it will enable all options (see descriptions)
|
||||||
|
func newDynamicOP(ctx context.Context, storage op.Storage, key [32]byte) (*op.Provider, error) {
|
||||||
|
config := &op.Config{
|
||||||
|
CryptoKey: key,
|
||||||
|
|
||||||
|
//will be used if the end_session endpoint is called without a post_logout_redirect_uri
|
||||||
|
DefaultLogoutRedirectURI: pathLoggedOut,
|
||||||
|
|
||||||
|
//enables code_challenge_method S256 for PKCE (and therefore PKCE in general)
|
||||||
|
CodeMethodS256: true,
|
||||||
|
|
||||||
|
//enables additional client_id/client_secret authentication by form post (not only HTTP Basic Auth)
|
||||||
|
AuthMethodPost: true,
|
||||||
|
|
||||||
|
//enables additional authentication by using private_key_jwt
|
||||||
|
AuthMethodPrivateKeyJWT: true,
|
||||||
|
|
||||||
|
//enables refresh_token grant use
|
||||||
|
GrantTypeRefreshToken: true,
|
||||||
|
|
||||||
|
//enables use of the `request` Object parameter
|
||||||
|
RequestObjectSupported: true,
|
||||||
|
|
||||||
|
//this example has only static texts (in English), so we'll set the here accordingly
|
||||||
|
SupportedUILocales: []language.Tag{language.English},
|
||||||
|
}
|
||||||
|
handler, err := op.NewDynamicOpenIDProvider("/", config, storage,
|
||||||
|
//we must explicitly allow the use of the http issuer
|
||||||
|
op.WithAllowInsecure(),
|
||||||
|
//as an example on how to customize an endpoint this will change the authorization_endpoint from /authorize to /auth
|
||||||
|
op.WithCustomAuthEndpoint(op.NewEndpoint("auth")),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return handler, nil
|
||||||
|
}
|
191
example/server/exampleop/device.go
Normal file
191
example/server/exampleop/device.go
Normal file
|
@ -0,0 +1,191 @@
|
||||||
|
package exampleop
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/gorilla/securecookie"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"github.com/zitadel/oidc/v2/pkg/op"
|
||||||
|
)
|
||||||
|
|
||||||
|
type deviceAuthenticate interface {
|
||||||
|
CheckUsernamePasswordSimple(username, password string) error
|
||||||
|
op.DeviceAuthorizationStorage
|
||||||
|
}
|
||||||
|
|
||||||
|
type deviceLogin struct {
|
||||||
|
storage deviceAuthenticate
|
||||||
|
cookie *securecookie.SecureCookie
|
||||||
|
}
|
||||||
|
|
||||||
|
func registerDeviceAuth(storage deviceAuthenticate, router *mux.Router) {
|
||||||
|
l := &deviceLogin{
|
||||||
|
storage: storage,
|
||||||
|
cookie: securecookie.New(securecookie.GenerateRandomKey(32), nil),
|
||||||
|
}
|
||||||
|
|
||||||
|
router.HandleFunc("", l.userCodeHandler)
|
||||||
|
router.Path("/login").Methods(http.MethodPost).HandlerFunc(l.loginHandler)
|
||||||
|
router.HandleFunc("/confirm", l.confirmHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderUserCode(w io.Writer, err error) {
|
||||||
|
data := struct {
|
||||||
|
Error string
|
||||||
|
}{
|
||||||
|
Error: errMsg(err),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := templates.ExecuteTemplate(w, "usercode", data); err != nil {
|
||||||
|
logrus.Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderDeviceLogin(w http.ResponseWriter, userCode string, err error) {
|
||||||
|
data := &struct {
|
||||||
|
UserCode string
|
||||||
|
Error string
|
||||||
|
}{
|
||||||
|
UserCode: userCode,
|
||||||
|
Error: errMsg(err),
|
||||||
|
}
|
||||||
|
if err = templates.ExecuteTemplate(w, "device_login", data); err != nil {
|
||||||
|
logrus.Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderConfirmPage(w http.ResponseWriter, username, clientID string, scopes []string) {
|
||||||
|
data := &struct {
|
||||||
|
Username string
|
||||||
|
ClientID string
|
||||||
|
Scopes []string
|
||||||
|
}{
|
||||||
|
Username: username,
|
||||||
|
ClientID: clientID,
|
||||||
|
Scopes: scopes,
|
||||||
|
}
|
||||||
|
if err := templates.ExecuteTemplate(w, "confirm_device", data); err != nil {
|
||||||
|
logrus.Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *deviceLogin) userCodeHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
err := r.ParseForm()
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
renderUserCode(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
userCode := r.Form.Get("user_code")
|
||||||
|
if userCode == "" {
|
||||||
|
if prompt, _ := url.QueryUnescape(r.Form.Get("prompt")); prompt != "" {
|
||||||
|
err = errors.New(prompt)
|
||||||
|
}
|
||||||
|
renderUserCode(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
renderDeviceLogin(w, userCode, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func redirectBack(w http.ResponseWriter, r *http.Request, prompt string) {
|
||||||
|
values := make(url.Values)
|
||||||
|
values.Set("prompt", url.QueryEscape(prompt))
|
||||||
|
|
||||||
|
url := url.URL{
|
||||||
|
Path: "/device",
|
||||||
|
RawQuery: values.Encode(),
|
||||||
|
}
|
||||||
|
http.Redirect(w, r, url.String(), http.StatusSeeOther)
|
||||||
|
}
|
||||||
|
|
||||||
|
const userCodeCookieName = "user_code"
|
||||||
|
|
||||||
|
type userCodeCookie struct {
|
||||||
|
UserCode string
|
||||||
|
UserName string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *deviceLogin) loginHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if err := r.ParseForm(); err != nil {
|
||||||
|
redirectBack(w, r, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userCode := r.PostForm.Get("user_code")
|
||||||
|
if userCode == "" {
|
||||||
|
redirectBack(w, r, "missing user_code in request")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
username := r.PostForm.Get("username")
|
||||||
|
if username == "" {
|
||||||
|
redirectBack(w, r, "missing username in request")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
password := r.PostForm.Get("password")
|
||||||
|
if password == "" {
|
||||||
|
redirectBack(w, r, "missing password in request")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := d.storage.CheckUsernamePasswordSimple(username, password); err != nil {
|
||||||
|
redirectBack(w, r, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
state, err := d.storage.GetDeviceAuthorizationByUserCode(r.Context(), userCode)
|
||||||
|
if err != nil {
|
||||||
|
redirectBack(w, r, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
encoded, err := d.cookie.Encode(userCodeCookieName, userCodeCookie{userCode, username})
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cookie := &http.Cookie{
|
||||||
|
Name: userCodeCookieName,
|
||||||
|
Value: encoded,
|
||||||
|
Path: "/",
|
||||||
|
}
|
||||||
|
http.SetCookie(w, cookie)
|
||||||
|
renderConfirmPage(w, username, state.ClientID, state.Scopes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *deviceLogin) confirmHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
cookie, err := r.Cookie(userCodeCookieName)
|
||||||
|
if err != nil {
|
||||||
|
redirectBack(w, r, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data := new(userCodeCookie)
|
||||||
|
if err = d.cookie.Decode(userCodeCookieName, cookie.Value, &data); err != nil {
|
||||||
|
redirectBack(w, r, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = r.ParseForm(); err != nil {
|
||||||
|
redirectBack(w, r, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
action := r.Form.Get("action")
|
||||||
|
switch action {
|
||||||
|
case "allowed":
|
||||||
|
err = d.storage.CompleteDeviceAuthorization(r.Context(), data.UserCode, data.UserName)
|
||||||
|
case "denied":
|
||||||
|
err = d.storage.DenyDeviceAuthorization(r.Context(), data.UserCode)
|
||||||
|
default:
|
||||||
|
err = errors.New("action must be one of \"allow\" or \"deny\"")
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
redirectBack(w, r, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintf(w, "Device authorization %s. You can now return to the device", action)
|
||||||
|
}
|
|
@ -1,53 +1,20 @@
|
||||||
package exampleop
|
package exampleop
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
queryAuthRequestID = "authRequestID"
|
|
||||||
)
|
|
||||||
|
|
||||||
var loginTmpl, _ = template.New("login").Parse(`
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>Login</title>
|
|
||||||
</head>
|
|
||||||
<body style="display: flex; align-items: center; justify-content: center; height: 100vh;">
|
|
||||||
<form method="POST" action="/login/username" style="height: 200px; width: 200px;">
|
|
||||||
|
|
||||||
<input type="hidden" name="id" value="{{.ID}}">
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label for="username">Username:</label>
|
|
||||||
<input id="username" name="username" style="width: 100%">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label for="password">Password:</label>
|
|
||||||
<input id="password" name="password" style="width: 100%">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p style="color:red; min-height: 1rem;">{{.Error}}</p>
|
|
||||||
|
|
||||||
<button type="submit">Login</button>
|
|
||||||
</form>
|
|
||||||
</body>
|
|
||||||
</html>`)
|
|
||||||
|
|
||||||
type login struct {
|
type login struct {
|
||||||
authenticate authenticate
|
authenticate authenticate
|
||||||
router *mux.Router
|
router *mux.Router
|
||||||
callback func(string) string
|
callback func(context.Context, string) string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewLogin(authenticate authenticate, callback func(string) string) *login {
|
func NewLogin(authenticate authenticate, callback func(context.Context, string) string) *login {
|
||||||
l := &login{
|
l := &login{
|
||||||
authenticate: authenticate,
|
authenticate: authenticate,
|
||||||
callback: callback,
|
callback: callback,
|
||||||
|
@ -78,18 +45,14 @@ func (l *login) loginHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func renderLogin(w http.ResponseWriter, id string, err error) {
|
func renderLogin(w http.ResponseWriter, id string, err error) {
|
||||||
var errMsg string
|
|
||||||
if err != nil {
|
|
||||||
errMsg = err.Error()
|
|
||||||
}
|
|
||||||
data := &struct {
|
data := &struct {
|
||||||
ID string
|
ID string
|
||||||
Error string
|
Error string
|
||||||
}{
|
}{
|
||||||
ID: id,
|
ID: id,
|
||||||
Error: errMsg,
|
Error: errMsg(err),
|
||||||
}
|
}
|
||||||
err = loginTmpl.Execute(w, data)
|
err = templates.ExecuteTemplate(w, "login", data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
|
@ -109,5 +72,5 @@ func (l *login) checkLoginHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
renderLogin(w, id, err)
|
renderLogin(w, id, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
http.Redirect(w, r, l.callback(id), http.StatusFound)
|
http.Redirect(w, r, l.callback(r.Context(), id), http.StatusFound)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,17 +1,16 @@
|
||||||
package exampleop
|
package exampleop
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"time"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"golang.org/x/text/language"
|
"golang.org/x/text/language"
|
||||||
|
|
||||||
"github.com/zitadel/oidc/example/server/storage"
|
"github.com/zitadel/oidc/v2/example/server/storage"
|
||||||
"github.com/zitadel/oidc/pkg/op"
|
"github.com/zitadel/oidc/v2/pkg/op"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -28,16 +27,14 @@ func init() {
|
||||||
|
|
||||||
type Storage interface {
|
type Storage interface {
|
||||||
op.Storage
|
op.Storage
|
||||||
CheckUsernamePassword(username, password, id string) error
|
authenticate
|
||||||
|
deviceAuthenticate
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetupServer creates an OIDC server with Issuer=http://localhost:<port>
|
// SetupServer creates an OIDC server with Issuer=http://localhost:<port>
|
||||||
//
|
//
|
||||||
// Use one of the pre-made clients in storage/clients.go or register a new one.
|
// Use one of the pre-made clients in storage/clients.go or register a new one.
|
||||||
func SetupServer(ctx context.Context, issuer string, storage Storage) *mux.Router {
|
func SetupServer(issuer string, storage Storage) *mux.Router {
|
||||||
// this will allow us to use an issuer with http:// instead of https://
|
|
||||||
os.Setenv(op.OidcDevMode, "true")
|
|
||||||
|
|
||||||
// the OpenID Provider requires a 32-byte key for (token) encryption
|
// the OpenID Provider requires a 32-byte key for (token) encryption
|
||||||
// be sure to create a proper crypto random key and manage it securely!
|
// be sure to create a proper crypto random key and manage it securely!
|
||||||
key := sha256.Sum256([]byte("test"))
|
key := sha256.Sum256([]byte("test"))
|
||||||
|
@ -53,7 +50,7 @@ func SetupServer(ctx context.Context, issuer string, storage Storage) *mux.Route
|
||||||
})
|
})
|
||||||
|
|
||||||
// creation of the OpenIDProvider with the just created in-memory Storage
|
// creation of the OpenIDProvider with the just created in-memory Storage
|
||||||
provider, err := newOP(ctx, storage, issuer, key)
|
provider, err := newOP(storage, issuer, key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
@ -66,6 +63,9 @@ func SetupServer(ctx context.Context, issuer string, storage Storage) *mux.Route
|
||||||
// so we will direct all calls to /login to the login UI
|
// so we will direct all calls to /login to the login UI
|
||||||
router.PathPrefix("/login/").Handler(http.StripPrefix("/login", l.router))
|
router.PathPrefix("/login/").Handler(http.StripPrefix("/login", l.router))
|
||||||
|
|
||||||
|
router.PathPrefix("/device").Subrouter()
|
||||||
|
registerDeviceAuth(storage, router.PathPrefix("/device").Subrouter())
|
||||||
|
|
||||||
// we register the http handler of the OP on the root, so that the discovery endpoint (/.well-known/openid-configuration)
|
// we register the http handler of the OP on the root, so that the discovery endpoint (/.well-known/openid-configuration)
|
||||||
// is served on the correct path
|
// is served on the correct path
|
||||||
//
|
//
|
||||||
|
@ -79,9 +79,8 @@ func SetupServer(ctx context.Context, issuer string, storage Storage) *mux.Route
|
||||||
// newOP will create an OpenID Provider for localhost on a specified port with a given encryption key
|
// newOP will create an OpenID Provider for localhost on a specified port with a given encryption key
|
||||||
// and a predefined default logout uri
|
// and a predefined default logout uri
|
||||||
// it will enable all options (see descriptions)
|
// it will enable all options (see descriptions)
|
||||||
func newOP(ctx context.Context, storage op.Storage, issuer string, key [32]byte) (op.OpenIDProvider, error) {
|
func newOP(storage op.Storage, issuer string, key [32]byte) (op.OpenIDProvider, error) {
|
||||||
config := &op.Config{
|
config := &op.Config{
|
||||||
Issuer: issuer,
|
|
||||||
CryptoKey: key,
|
CryptoKey: key,
|
||||||
|
|
||||||
// will be used if the end_session endpoint is called without a post_logout_redirect_uri
|
// will be used if the end_session endpoint is called without a post_logout_redirect_uri
|
||||||
|
@ -104,8 +103,17 @@ func newOP(ctx context.Context, storage op.Storage, issuer string, key [32]byte)
|
||||||
|
|
||||||
// this example has only static texts (in English), so we'll set the here accordingly
|
// this example has only static texts (in English), so we'll set the here accordingly
|
||||||
SupportedUILocales: []language.Tag{language.English},
|
SupportedUILocales: []language.Tag{language.English},
|
||||||
|
|
||||||
|
DeviceAuthorization: op.DeviceAuthorizationConfig{
|
||||||
|
Lifetime: 5 * time.Minute,
|
||||||
|
PollInterval: 5 * time.Second,
|
||||||
|
UserFormURL: issuer + "device",
|
||||||
|
UserCode: op.UserCodeBase20,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
handler, err := op.NewOpenIDProvider(ctx, config, storage,
|
handler, err := op.NewOpenIDProvider(issuer, config, storage,
|
||||||
|
//we must explicitly allow the use of the http issuer
|
||||||
|
op.WithAllowInsecure(),
|
||||||
// as an example on how to customize an endpoint this will change the authorization_endpoint from /authorize to /auth
|
// as an example on how to customize an endpoint this will change the authorization_endpoint from /authorize to /auth
|
||||||
op.WithCustomAuthEndpoint(op.NewEndpoint("auth")),
|
op.WithCustomAuthEndpoint(op.NewEndpoint("auth")),
|
||||||
)
|
)
|
||||||
|
|
26
example/server/exampleop/templates.go
Normal file
26
example/server/exampleop/templates.go
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
package exampleop
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
"html/template"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
//go:embed templates
|
||||||
|
templateFS embed.FS
|
||||||
|
templates = template.Must(template.ParseFS(templateFS, "templates/*.html"))
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
queryAuthRequestID = "authRequestID"
|
||||||
|
)
|
||||||
|
|
||||||
|
func errMsg(err error) string {
|
||||||
|
if err == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
logrus.Error(err)
|
||||||
|
return err.Error()
|
||||||
|
}
|
25
example/server/exampleop/templates/confirm_device.html
Normal file
25
example/server/exampleop/templates/confirm_device.html
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
{{ define "confirm_device" -}}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Confirm device authorization</title>
|
||||||
|
<style>
|
||||||
|
.green{
|
||||||
|
background-color: green
|
||||||
|
}
|
||||||
|
.red{
|
||||||
|
background-color: red
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Welcome back {{.Username}}!</h1>
|
||||||
|
<p>
|
||||||
|
You are about to grant device {{.ClientID}} access to the following scopes: {{.Scopes}}.
|
||||||
|
</p>
|
||||||
|
<button onclick="location.href='./confirm?action=allowed'" type="button" class="green">Allow</button>
|
||||||
|
<button onclick="location.href='./confirm?action=denied'" type="button" class="red">Deny</button>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
{{- end }}
|
29
example/server/exampleop/templates/device_login.html
Normal file
29
example/server/exampleop/templates/device_login.html
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
{{ define "device_login" -}}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Login</title>
|
||||||
|
</head>
|
||||||
|
<body style="display: flex; align-items: center; justify-content: center; height: 100vh;">
|
||||||
|
<form method="POST" action="/device/login" style="height: 200px; width: 200px;">
|
||||||
|
|
||||||
|
<input type="hidden" name="user_code" value="{{.UserCode}}">
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="username">Username:</label>
|
||||||
|
<input id="username" name="username" style="width: 100%">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="password">Password:</label>
|
||||||
|
<input id="password" name="password" style="width: 100%">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p style="color:red; min-height: 1rem;">{{.Error}}</p>
|
||||||
|
|
||||||
|
<button type="submit">Login</button>
|
||||||
|
</form>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
{{- end }}
|
29
example/server/exampleop/templates/login.html
Normal file
29
example/server/exampleop/templates/login.html
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
{{ define "login" -}}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Login</title>
|
||||||
|
</head>
|
||||||
|
<body style="display: flex; align-items: center; justify-content: center; height: 100vh;">
|
||||||
|
<form method="POST" action="/login/username" style="height: 200px; width: 200px;">
|
||||||
|
|
||||||
|
<input type="hidden" name="id" value="{{.ID}}">
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="username">Username:</label>
|
||||||
|
<input id="username" name="username" style="width: 100%">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="password">Password:</label>
|
||||||
|
<input id="password" name="password" style="width: 100%">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p style="color:red; min-height: 1rem;">{{.Error}}</p>
|
||||||
|
|
||||||
|
<button type="submit">Login</button>
|
||||||
|
</form>
|
||||||
|
</body>
|
||||||
|
</html>`
|
||||||
|
{{- end }}
|
21
example/server/exampleop/templates/usercode.html
Normal file
21
example/server/exampleop/templates/usercode.html
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
{{ define "usercode" -}}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Device authorization</title>
|
||||||
|
</head>
|
||||||
|
<body style="display: flex; align-items: center; justify-content: center; height: 100vh;">
|
||||||
|
<form method="POST" style="height: 200px; width: 200px;">
|
||||||
|
<h1>Device authorization</h1>
|
||||||
|
<div>
|
||||||
|
<label for="user_code">Code:</label>
|
||||||
|
<input id="user_code" name="user_code" style="width: 100%">
|
||||||
|
</div>
|
||||||
|
<p style="color:red; min-height: 1rem;">{{.Error}}</p>
|
||||||
|
|
||||||
|
<button type="submit">Login</button>
|
||||||
|
</form>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
{{- end }}
|
|
@ -1,24 +1,26 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/zitadel/oidc/example/server/exampleop"
|
"github.com/zitadel/oidc/v2/example/server/exampleop"
|
||||||
"github.com/zitadel/oidc/example/server/storage"
|
"github.com/zitadel/oidc/v2/example/server/storage"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
ctx := context.Background()
|
//we will run on :9998
|
||||||
|
port := "9998"
|
||||||
|
//which gives us the issuer: http://localhost:9998/
|
||||||
|
issuer := fmt.Sprintf("http://localhost:%s/", port)
|
||||||
|
|
||||||
// the OpenIDProvider interface needs a Storage interface handling various checks and state manipulations
|
// the OpenIDProvider interface needs a Storage interface handling various checks and state manipulations
|
||||||
// this might be the layer for accessing your database
|
// this might be the layer for accessing your database
|
||||||
// in this example it will be handled in-memory
|
// in this example it will be handled in-memory
|
||||||
storage := storage.NewStorage(storage.NewUserStore())
|
storage := storage.NewStorage(storage.NewUserStore(issuer))
|
||||||
|
|
||||||
port := "9998"
|
router := exampleop.SetupServer(issuer, storage)
|
||||||
router := exampleop.SetupServer(ctx, "http://localhost:"+port, storage)
|
|
||||||
|
|
||||||
server := &http.Server{
|
server := &http.Server{
|
||||||
Addr: ":" + port,
|
Addr: ":" + port,
|
||||||
|
@ -30,5 +32,4 @@ func main() {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
<-ctx.Done()
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,8 +3,8 @@ package storage
|
||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/zitadel/oidc/pkg/oidc"
|
"github.com/zitadel/oidc/v2/pkg/oidc"
|
||||||
"github.com/zitadel/oidc/pkg/op"
|
"github.com/zitadel/oidc/v2/pkg/op"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -168,7 +168,7 @@ func NativeClient(id string, redirectURIs ...string) *Client {
|
||||||
loginURL: defaultLoginURL,
|
loginURL: defaultLoginURL,
|
||||||
responseTypes: []oidc.ResponseType{oidc.ResponseTypeCode},
|
responseTypes: []oidc.ResponseType{oidc.ResponseTypeCode},
|
||||||
grantTypes: []oidc.GrantType{oidc.GrantTypeCode, oidc.GrantTypeRefreshToken},
|
grantTypes: []oidc.GrantType{oidc.GrantTypeCode, oidc.GrantTypeRefreshToken},
|
||||||
accessTokenType: 0,
|
accessTokenType: op.AccessTokenTypeBearer,
|
||||||
devMode: false,
|
devMode: false,
|
||||||
idTokenUserinfoClaimsAssertion: false,
|
idTokenUserinfoClaimsAssertion: false,
|
||||||
clockSkew: 0,
|
clockSkew: 0,
|
||||||
|
@ -194,7 +194,7 @@ func WebClient(id, secret string, redirectURIs ...string) *Client {
|
||||||
loginURL: defaultLoginURL,
|
loginURL: defaultLoginURL,
|
||||||
responseTypes: []oidc.ResponseType{oidc.ResponseTypeCode},
|
responseTypes: []oidc.ResponseType{oidc.ResponseTypeCode},
|
||||||
grantTypes: []oidc.GrantType{oidc.GrantTypeCode, oidc.GrantTypeRefreshToken},
|
grantTypes: []oidc.GrantType{oidc.GrantTypeCode, oidc.GrantTypeRefreshToken},
|
||||||
accessTokenType: 0,
|
accessTokenType: op.AccessTokenTypeBearer,
|
||||||
devMode: false,
|
devMode: false,
|
||||||
idTokenUserinfoClaimsAssertion: false,
|
idTokenUserinfoClaimsAssertion: false,
|
||||||
clockSkew: 0,
|
clockSkew: 0,
|
||||||
|
|
|
@ -5,9 +5,8 @@ import (
|
||||||
|
|
||||||
"golang.org/x/text/language"
|
"golang.org/x/text/language"
|
||||||
|
|
||||||
"github.com/zitadel/oidc/pkg/op"
|
"github.com/zitadel/oidc/v2/pkg/oidc"
|
||||||
|
"github.com/zitadel/oidc/v2/pkg/op"
|
||||||
"github.com/zitadel/oidc/pkg/oidc"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -17,6 +16,9 @@ const (
|
||||||
|
|
||||||
// CustomClaim is an example for how to return custom claims with this library
|
// CustomClaim is an example for how to return custom claims with this library
|
||||||
CustomClaim = "custom_claim"
|
CustomClaim = "custom_claim"
|
||||||
|
|
||||||
|
// CustomScopeImpersonatePrefix is an example scope prefix for passing user id to impersonate using token exchage
|
||||||
|
CustomScopeImpersonatePrefix = "custom_scope:impersonate:"
|
||||||
)
|
)
|
||||||
|
|
||||||
type AuthRequest struct {
|
type AuthRequest struct {
|
||||||
|
@ -35,7 +37,7 @@ type AuthRequest struct {
|
||||||
Nonce string
|
Nonce string
|
||||||
CodeChallenge *OIDCCodeChallenge
|
CodeChallenge *OIDCCodeChallenge
|
||||||
|
|
||||||
passwordChecked bool
|
done bool
|
||||||
authTime time.Time
|
authTime time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,7 +51,7 @@ func (a *AuthRequest) GetACR() string {
|
||||||
|
|
||||||
func (a *AuthRequest) GetAMR() []string {
|
func (a *AuthRequest) GetAMR() []string {
|
||||||
// this example only uses password for authentication
|
// this example only uses password for authentication
|
||||||
if a.passwordChecked {
|
if a.done {
|
||||||
return []string{"pwd"}
|
return []string{"pwd"}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
@ -100,7 +102,7 @@ func (a *AuthRequest) GetSubject() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *AuthRequest) Done() bool {
|
func (a *AuthRequest) Done() bool {
|
||||||
return a.passwordChecked // this example only uses password for authentication
|
return a.done
|
||||||
}
|
}
|
||||||
|
|
||||||
func PromptToInternal(oidcPrompt oidc.SpaceDelimitedArray) []string {
|
func PromptToInternal(oidcPrompt oidc.SpaceDelimitedArray) []string {
|
||||||
|
|
|
@ -4,16 +4,18 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/big"
|
"math/big"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"gopkg.in/square/go-jose.v2"
|
"gopkg.in/square/go-jose.v2"
|
||||||
|
|
||||||
"github.com/zitadel/oidc/pkg/oidc"
|
"github.com/zitadel/oidc/v2/pkg/oidc"
|
||||||
"github.com/zitadel/oidc/pkg/op"
|
"github.com/zitadel/oidc/v2/pkg/op"
|
||||||
)
|
)
|
||||||
|
|
||||||
// serviceKey1 is a public key which will be used for the JWT Profile Authorization Grant
|
// serviceKey1 is a public key which will be used for the JWT Profile Authorization Grant
|
||||||
|
@ -26,8 +28,8 @@ var serviceKey1 = &rsa.PublicKey{
|
||||||
E: 65537,
|
E: 65537,
|
||||||
}
|
}
|
||||||
|
|
||||||
// var _ op.Storage = &storage{}
|
var _ op.Storage = &Storage{}
|
||||||
// var _ op.ClientCredentialsStorage = &storage{}
|
var _ op.ClientCredentialsStorage = &Storage{}
|
||||||
|
|
||||||
// storage implements the op.Storage interface
|
// storage implements the op.Storage interface
|
||||||
// typically you would implement this as a layer on top of your database
|
// typically you would implement this as a layer on top of your database
|
||||||
|
@ -42,12 +44,47 @@ type Storage struct {
|
||||||
services map[string]Service
|
services map[string]Service
|
||||||
refreshTokens map[string]*RefreshToken
|
refreshTokens map[string]*RefreshToken
|
||||||
signingKey signingKey
|
signingKey signingKey
|
||||||
|
deviceCodes map[string]deviceAuthorizationEntry
|
||||||
|
userCodes map[string]string
|
||||||
|
serviceUsers map[string]*Client
|
||||||
}
|
}
|
||||||
|
|
||||||
type signingKey struct {
|
type signingKey struct {
|
||||||
ID string
|
id string
|
||||||
Algorithm string
|
algorithm jose.SignatureAlgorithm
|
||||||
Key *rsa.PrivateKey
|
key *rsa.PrivateKey
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *signingKey) SignatureAlgorithm() jose.SignatureAlgorithm {
|
||||||
|
return s.algorithm
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *signingKey) Key() interface{} {
|
||||||
|
return s.key
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *signingKey) ID() string {
|
||||||
|
return s.id
|
||||||
|
}
|
||||||
|
|
||||||
|
type publicKey struct {
|
||||||
|
signingKey
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *publicKey) ID() string {
|
||||||
|
return s.id
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *publicKey) Algorithm() jose.SignatureAlgorithm {
|
||||||
|
return s.algorithm
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *publicKey) Use() string {
|
||||||
|
return "sig"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *publicKey) Key() interface{} {
|
||||||
|
return &s.key.PublicKey
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewStorage(userStore UserStore) *Storage {
|
func NewStorage(userStore UserStore) *Storage {
|
||||||
|
@ -67,9 +104,21 @@ func NewStorage(userStore UserStore) *Storage {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
signingKey: signingKey{
|
signingKey: signingKey{
|
||||||
ID: "id",
|
id: uuid.NewString(),
|
||||||
Algorithm: "RS256",
|
algorithm: jose.RS256,
|
||||||
Key: key,
|
key: key,
|
||||||
|
},
|
||||||
|
deviceCodes: make(map[string]deviceAuthorizationEntry),
|
||||||
|
userCodes: make(map[string]string),
|
||||||
|
serviceUsers: map[string]*Client{
|
||||||
|
"sid1": {
|
||||||
|
id: "sid1",
|
||||||
|
secret: "verysecret",
|
||||||
|
grantTypes: []oidc.GrantType{
|
||||||
|
oidc.GrantTypeClientCredentials,
|
||||||
|
},
|
||||||
|
accessTokenType: op.AccessTokenTypeBearer,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -95,7 +144,18 @@ func (s *Storage) CheckUsernamePassword(username, password, id string) error {
|
||||||
// you will have to change some state on the request to guide the user through possible multiple steps of the login process
|
// you will have to change some state on the request to guide the user through possible multiple steps of the login process
|
||||||
// in this example we'll simply check the username / password and set a boolean to true
|
// in this example we'll simply check the username / password and set a boolean to true
|
||||||
// therefore we will also just check this boolean if the request / login has been finished
|
// therefore we will also just check this boolean if the request / login has been finished
|
||||||
request.passwordChecked = true
|
request.done = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("username or password wrong")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Storage) CheckUsernamePasswordSimple(username, password string) error {
|
||||||
|
s.lock.Lock()
|
||||||
|
defer s.lock.Unlock()
|
||||||
|
|
||||||
|
user := s.userStore.GetUserByUsername(username)
|
||||||
|
if user != nil && user.Password == password {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return fmt.Errorf("username or password wrong")
|
return fmt.Errorf("username or password wrong")
|
||||||
|
@ -181,11 +241,14 @@ func (s *Storage) DeleteAuthRequest(ctx context.Context, id string) error {
|
||||||
// it will be called for all requests able to return an access token (Authorization Code Flow, Implicit Flow, JWT Profile, ...)
|
// it will be called for all requests able to return an access token (Authorization Code Flow, Implicit Flow, JWT Profile, ...)
|
||||||
func (s *Storage) CreateAccessToken(ctx context.Context, request op.TokenRequest) (string, time.Time, error) {
|
func (s *Storage) CreateAccessToken(ctx context.Context, request op.TokenRequest) (string, time.Time, error) {
|
||||||
var applicationID string
|
var applicationID string
|
||||||
|
switch req := request.(type) {
|
||||||
|
case *AuthRequest:
|
||||||
// if authenticated for an app (auth code / implicit flow) we must save the client_id to the token
|
// if authenticated for an app (auth code / implicit flow) we must save the client_id to the token
|
||||||
authReq, ok := request.(*AuthRequest)
|
applicationID = req.ApplicationID
|
||||||
if ok {
|
case op.TokenExchangeRequest:
|
||||||
applicationID = authReq.ApplicationID
|
applicationID = req.GetClientID()
|
||||||
}
|
}
|
||||||
|
|
||||||
token, err := s.accessToken(applicationID, "", request.GetSubject(), request.GetAudience(), request.GetScopes())
|
token, err := s.accessToken(applicationID, "", request.GetSubject(), request.GetAudience(), request.GetScopes())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", time.Time{}, err
|
return "", time.Time{}, err
|
||||||
|
@ -196,6 +259,11 @@ func (s *Storage) CreateAccessToken(ctx context.Context, request op.TokenRequest
|
||||||
// CreateAccessAndRefreshTokens implements the op.Storage interface
|
// CreateAccessAndRefreshTokens implements the op.Storage interface
|
||||||
// it will be called for all requests able to return an access and refresh token (Authorization Code Flow, Refresh Token Request)
|
// it will be called for all requests able to return an access and refresh token (Authorization Code Flow, Refresh Token Request)
|
||||||
func (s *Storage) CreateAccessAndRefreshTokens(ctx context.Context, request op.TokenRequest, currentRefreshToken string) (accessTokenID string, newRefreshToken string, expiration time.Time, err error) {
|
func (s *Storage) CreateAccessAndRefreshTokens(ctx context.Context, request op.TokenRequest, currentRefreshToken string) (accessTokenID string, newRefreshToken string, expiration time.Time, err error) {
|
||||||
|
// generate tokens via token exchange flow if request is relevant
|
||||||
|
if teReq, ok := request.(op.TokenExchangeRequest); ok {
|
||||||
|
return s.exchangeRefreshToken(ctx, teReq)
|
||||||
|
}
|
||||||
|
|
||||||
// get the information depending on the request type / implementation
|
// get the information depending on the request type / implementation
|
||||||
applicationID, authTime, amr := getInfoFromRequest(request)
|
applicationID, authTime, amr := getInfoFromRequest(request)
|
||||||
|
|
||||||
|
@ -226,6 +294,24 @@ func (s *Storage) CreateAccessAndRefreshTokens(ctx context.Context, request op.T
|
||||||
return accessToken.ID, refreshToken, accessToken.Expiration, nil
|
return accessToken.ID, refreshToken, accessToken.Expiration, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Storage) exchangeRefreshToken(ctx context.Context, request op.TokenExchangeRequest) (accessTokenID string, newRefreshToken string, expiration time.Time, err error) {
|
||||||
|
applicationID := request.GetClientID()
|
||||||
|
authTime := request.GetAuthTime()
|
||||||
|
|
||||||
|
refreshTokenID := uuid.NewString()
|
||||||
|
accessToken, err := s.accessToken(applicationID, refreshTokenID, request.GetSubject(), request.GetAudience(), request.GetScopes())
|
||||||
|
if err != nil {
|
||||||
|
return "", "", time.Time{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshToken, err := s.createRefreshToken(accessToken, nil, authTime)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", time.Time{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return accessToken.ID, refreshToken, accessToken.Expiration, nil
|
||||||
|
}
|
||||||
|
|
||||||
// TokenRequestByRefreshToken implements the op.Storage interface
|
// TokenRequestByRefreshToken implements the op.Storage interface
|
||||||
// it will be called after parsing and validation of the refresh token request
|
// it will be called after parsing and validation of the refresh token request
|
||||||
func (s *Storage) TokenRequestByRefreshToken(ctx context.Context, refreshToken string) (op.RefreshTokenRequest, error) {
|
func (s *Storage) TokenRequestByRefreshToken(ctx context.Context, refreshToken string) (op.RefreshTokenRequest, error) {
|
||||||
|
@ -252,6 +338,16 @@ func (s *Storage) TerminateSession(ctx context.Context, userID string, clientID
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetRefreshTokenInfo looks up a refresh token and returns the token id and user id.
|
||||||
|
// If given something that is not a refresh token, it must return error.
|
||||||
|
func (s *Storage) GetRefreshTokenInfo(ctx context.Context, clientID string, token string) (userID string, tokenID string, err error) {
|
||||||
|
refreshToken, ok := s.refreshTokens[token]
|
||||||
|
if !ok {
|
||||||
|
return "", "", op.ErrInvalidRefreshToken
|
||||||
|
}
|
||||||
|
return refreshToken.UserID, refreshToken.ID, nil
|
||||||
|
}
|
||||||
|
|
||||||
// RevokeToken implements the op.Storage interface
|
// RevokeToken implements the op.Storage interface
|
||||||
// it will be called after parsing and validation of the token revocation request
|
// it will be called after parsing and validation of the token revocation request
|
||||||
func (s *Storage) RevokeToken(ctx context.Context, tokenIDOrToken string, userID string, clientID string) *oidc.Error {
|
func (s *Storage) RevokeToken(ctx context.Context, tokenIDOrToken string, userID string, clientID string) *oidc.Error {
|
||||||
|
@ -288,41 +384,29 @@ func (s *Storage) RevokeToken(ctx context.Context, tokenIDOrToken string, userID
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetSigningKey implements the op.Storage interface
|
// SigningKey implements the op.Storage interface
|
||||||
// it will be called when creating the OpenID Provider
|
// it will be called when creating the OpenID Provider
|
||||||
func (s *Storage) GetSigningKey(ctx context.Context, keyCh chan<- jose.SigningKey) {
|
func (s *Storage) SigningKey(ctx context.Context) (op.SigningKey, error) {
|
||||||
// in this example the signing key is a static rsa.PrivateKey and the algorithm used is RS256
|
// in this example the signing key is a static rsa.PrivateKey and the algorithm used is RS256
|
||||||
// you would obviously have a more complex implementation and store / retrieve the key from your database as well
|
// you would obviously have a more complex implementation and store / retrieve the key from your database as well
|
||||||
//
|
return &s.signingKey, nil
|
||||||
// the idea of the signing key channel is, that you can (with what ever mechanism) rotate your signing key and
|
|
||||||
// switch the key of the signer via this channel
|
|
||||||
keyCh <- jose.SigningKey{
|
|
||||||
Algorithm: jose.SignatureAlgorithm(s.signingKey.Algorithm), // always tell the signer with algorithm to use
|
|
||||||
Key: jose.JSONWebKey{
|
|
||||||
KeyID: s.signingKey.ID, // always give the key an id so, that it will include it in the token header as `kid` claim
|
|
||||||
Key: s.signingKey.Key,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetKeySet implements the op.Storage interface
|
// SignatureAlgorithms implements the op.Storage interface
|
||||||
|
// it will be called to get the sign
|
||||||
|
func (s *Storage) SignatureAlgorithms(context.Context) ([]jose.SignatureAlgorithm, error) {
|
||||||
|
return []jose.SignatureAlgorithm{s.signingKey.algorithm}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeySet implements the op.Storage interface
|
||||||
// it will be called to get the current (public) keys, among others for the keys_endpoint or for validating access_tokens on the userinfo_endpoint, ...
|
// it will be called to get the current (public) keys, among others for the keys_endpoint or for validating access_tokens on the userinfo_endpoint, ...
|
||||||
func (s *Storage) GetKeySet(ctx context.Context) (*jose.JSONWebKeySet, error) {
|
func (s *Storage) KeySet(ctx context.Context) ([]op.Key, error) {
|
||||||
// as mentioned above, this example only has a single signing key without key rotation,
|
// as mentioned above, this example only has a single signing key without key rotation,
|
||||||
// so it will directly use its public key
|
// so it will directly use its public key
|
||||||
//
|
//
|
||||||
// when using key rotation you typically would store the public keys alongside the private keys in your database
|
// when using key rotation you typically would store the public keys alongside the private keys in your database
|
||||||
// and give both of them an expiration date, with the public key having a longer lifetime (e.g. rotate private key every
|
// and give both of them an expiration date, with the public key having a longer lifetime
|
||||||
return &jose.JSONWebKeySet{
|
return []op.Key{&publicKey{s.signingKey}}, nil
|
||||||
Keys: []jose.JSONWebKey{
|
|
||||||
{
|
|
||||||
KeyID: s.signingKey.ID,
|
|
||||||
Algorithm: s.signingKey.Algorithm,
|
|
||||||
Use: oidc.KeyUseSignature,
|
|
||||||
Key: &s.signingKey.Key.PublicKey,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetClientByClientID implements the op.Storage interface
|
// GetClientByClientID implements the op.Storage interface
|
||||||
|
@ -356,13 +440,13 @@ func (s *Storage) AuthorizeClientIDSecret(ctx context.Context, clientID, clientS
|
||||||
|
|
||||||
// SetUserinfoFromScopes implements the op.Storage interface
|
// SetUserinfoFromScopes implements the op.Storage interface
|
||||||
// it will be called for the creation of an id_token, so we'll just pass it to the private function without any further check
|
// it will be called for the creation of an id_token, so we'll just pass it to the private function without any further check
|
||||||
func (s *Storage) SetUserinfoFromScopes(ctx context.Context, userinfo oidc.UserInfoSetter, userID, clientID string, scopes []string) error {
|
func (s *Storage) SetUserinfoFromScopes(ctx context.Context, userinfo *oidc.UserInfo, userID, clientID string, scopes []string) error {
|
||||||
return s.setUserinfo(ctx, userinfo, userID, clientID, scopes)
|
return s.setUserinfo(ctx, userinfo, userID, clientID, scopes)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetUserinfoFromToken implements the op.Storage interface
|
// SetUserinfoFromToken implements the op.Storage interface
|
||||||
// it will be called for the userinfo endpoint, so we read the token and pass the information from that to the private function
|
// it will be called for the userinfo endpoint, so we read the token and pass the information from that to the private function
|
||||||
func (s *Storage) SetUserinfoFromToken(ctx context.Context, userinfo oidc.UserInfoSetter, tokenID, subject, origin string) error {
|
func (s *Storage) SetUserinfoFromToken(ctx context.Context, userinfo *oidc.UserInfo, tokenID, subject, origin string) error {
|
||||||
token, ok := func() (*Token, bool) {
|
token, ok := func() (*Token, bool) {
|
||||||
s.lock.Lock()
|
s.lock.Lock()
|
||||||
defer s.lock.Unlock()
|
defer s.lock.Unlock()
|
||||||
|
@ -390,7 +474,7 @@ func (s *Storage) SetUserinfoFromToken(ctx context.Context, userinfo oidc.UserIn
|
||||||
|
|
||||||
// SetIntrospectionFromToken implements the op.Storage interface
|
// SetIntrospectionFromToken implements the op.Storage interface
|
||||||
// it will be called for the introspection endpoint, so we read the token and pass the information from that to the private function
|
// it will be called for the introspection endpoint, so we read the token and pass the information from that to the private function
|
||||||
func (s *Storage) SetIntrospectionFromToken(ctx context.Context, introspection oidc.IntrospectionResponse, tokenID, subject, clientID string) error {
|
func (s *Storage) SetIntrospectionFromToken(ctx context.Context, introspection *oidc.IntrospectionResponse, tokenID, subject, clientID string) error {
|
||||||
token, ok := func() (*Token, bool) {
|
token, ok := func() (*Token, bool) {
|
||||||
s.lock.Lock()
|
s.lock.Lock()
|
||||||
defer s.lock.Unlock()
|
defer s.lock.Unlock()
|
||||||
|
@ -407,14 +491,17 @@ func (s *Storage) SetIntrospectionFromToken(ctx context.Context, introspection o
|
||||||
// this will automatically be done by the library if you don't return an error
|
// this will automatically be done by the library if you don't return an error
|
||||||
// you can also return further information about the user / associated token
|
// you can also return further information about the user / associated token
|
||||||
// e.g. the userinfo (equivalent to userinfo endpoint)
|
// e.g. the userinfo (equivalent to userinfo endpoint)
|
||||||
err := s.setUserinfo(ctx, introspection, subject, clientID, token.Scopes)
|
|
||||||
|
userInfo := new(oidc.UserInfo)
|
||||||
|
err := s.setUserinfo(ctx, userInfo, subject, clientID, token.Scopes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
introspection.SetUserInfo(userInfo)
|
||||||
//...and also the requested scopes...
|
//...and also the requested scopes...
|
||||||
introspection.SetScopes(token.Scopes)
|
introspection.Scope = token.Scopes
|
||||||
//...and the client the token was issued to
|
//...and the client the token was issued to
|
||||||
introspection.SetClientID(token.ApplicationID)
|
introspection.ClientID = token.ApplicationID
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -424,6 +511,10 @@ func (s *Storage) SetIntrospectionFromToken(ctx context.Context, introspection o
|
||||||
// GetPrivateClaimsFromScopes implements the op.Storage interface
|
// GetPrivateClaimsFromScopes implements the op.Storage interface
|
||||||
// it will be called for the creation of a JWT access token to assert claims for custom scopes
|
// it will be called for the creation of a JWT access token to assert claims for custom scopes
|
||||||
func (s *Storage) GetPrivateClaimsFromScopes(ctx context.Context, userID, clientID string, scopes []string) (claims map[string]interface{}, err error) {
|
func (s *Storage) GetPrivateClaimsFromScopes(ctx context.Context, userID, clientID string, scopes []string) (claims map[string]interface{}, err error) {
|
||||||
|
return s.getPrivateClaimsFromScopes(ctx, userID, clientID, scopes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Storage) getPrivateClaimsFromScopes(ctx context.Context, userID, clientID string, scopes []string) (claims map[string]interface{}, err error) {
|
||||||
for _, scope := range scopes {
|
for _, scope := range scopes {
|
||||||
switch scope {
|
switch scope {
|
||||||
case CustomScope:
|
case CustomScope:
|
||||||
|
@ -433,9 +524,9 @@ func (s *Storage) GetPrivateClaimsFromScopes(ctx context.Context, userID, client
|
||||||
return claims, nil
|
return claims, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetKeyByIDAndUserID implements the op.Storage interface
|
// GetKeyByIDAndClientID implements the op.Storage interface
|
||||||
// it will be called to validate the signatures of a JWT (JWT Profile Grant and Authentication)
|
// it will be called to validate the signatures of a JWT (JWT Profile Grant and Authentication)
|
||||||
func (s *Storage) GetKeyByIDAndUserID(ctx context.Context, keyID, clientID string) (*jose.JSONWebKey, error) {
|
func (s *Storage) GetKeyByIDAndClientID(ctx context.Context, keyID, clientID string) (*jose.JSONWebKey, error) {
|
||||||
s.lock.Lock()
|
s.lock.Lock()
|
||||||
defer s.lock.Unlock()
|
defer s.lock.Unlock()
|
||||||
service, ok := s.services[clientID]
|
service, ok := s.services[clientID]
|
||||||
|
@ -531,7 +622,7 @@ func (s *Storage) accessToken(applicationID, refreshTokenID, subject string, aud
|
||||||
}
|
}
|
||||||
|
|
||||||
// setUserinfo sets the info based on the user, scopes and if necessary the clientID
|
// setUserinfo sets the info based on the user, scopes and if necessary the clientID
|
||||||
func (s *Storage) setUserinfo(ctx context.Context, userInfo oidc.UserInfoSetter, userID, clientID string, scopes []string) (err error) {
|
func (s *Storage) setUserinfo(ctx context.Context, userInfo *oidc.UserInfo, userID, clientID string, scopes []string) (err error) {
|
||||||
s.lock.Lock()
|
s.lock.Lock()
|
||||||
defer s.lock.Unlock()
|
defer s.lock.Unlock()
|
||||||
user := s.userStore.GetUserByID(userID)
|
user := s.userStore.GetUserByID(userID)
|
||||||
|
@ -541,17 +632,19 @@ func (s *Storage) setUserinfo(ctx context.Context, userInfo oidc.UserInfoSetter,
|
||||||
for _, scope := range scopes {
|
for _, scope := range scopes {
|
||||||
switch scope {
|
switch scope {
|
||||||
case oidc.ScopeOpenID:
|
case oidc.ScopeOpenID:
|
||||||
userInfo.SetSubject(user.ID)
|
userInfo.Subject = user.ID
|
||||||
case oidc.ScopeEmail:
|
case oidc.ScopeEmail:
|
||||||
userInfo.SetEmail(user.Email, user.EmailVerified)
|
userInfo.Email = user.Email
|
||||||
|
userInfo.EmailVerified = oidc.Bool(user.EmailVerified)
|
||||||
case oidc.ScopeProfile:
|
case oidc.ScopeProfile:
|
||||||
userInfo.SetPreferredUsername(user.Username)
|
userInfo.PreferredUsername = user.Username
|
||||||
userInfo.SetName(user.FirstName + " " + user.LastName)
|
userInfo.Name = user.FirstName + " " + user.LastName
|
||||||
userInfo.SetFamilyName(user.LastName)
|
userInfo.FamilyName = user.LastName
|
||||||
userInfo.SetGivenName(user.FirstName)
|
userInfo.GivenName = user.FirstName
|
||||||
userInfo.SetLocale(user.PreferredLanguage)
|
userInfo.Locale = oidc.NewLocale(user.PreferredLanguage)
|
||||||
case oidc.ScopePhone:
|
case oidc.ScopePhone:
|
||||||
userInfo.SetPhone(user.Phone, user.PhoneVerified)
|
userInfo.PhoneNumber = user.Phone
|
||||||
|
userInfo.PhoneNumberVerified = user.PhoneVerified
|
||||||
case CustomScope:
|
case CustomScope:
|
||||||
// you can also have a custom scope and assert public or custom claims based on that
|
// you can also have a custom scope and assert public or custom claims based on that
|
||||||
userInfo.AppendClaims(CustomClaim, customClaim(clientID))
|
userInfo.AppendClaims(CustomClaim, customClaim(clientID))
|
||||||
|
@ -560,6 +653,101 @@ func (s *Storage) setUserinfo(ctx context.Context, userInfo oidc.UserInfoSetter,
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ValidateTokenExchangeRequest implements the op.TokenExchangeStorage interface
|
||||||
|
// it will be called to validate parsed Token Exchange Grant request
|
||||||
|
func (s *Storage) ValidateTokenExchangeRequest(ctx context.Context, request op.TokenExchangeRequest) error {
|
||||||
|
if request.GetRequestedTokenType() == "" {
|
||||||
|
request.SetRequestedTokenType(oidc.RefreshTokenType)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Just an example, some use cases might need this use case
|
||||||
|
if request.GetExchangeSubjectTokenType() == oidc.IDTokenType && request.GetRequestedTokenType() == oidc.RefreshTokenType {
|
||||||
|
return errors.New("exchanging id_token to refresh_token is not supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check impersonation permissions
|
||||||
|
if request.GetExchangeActor() == "" && !s.userStore.GetUserByID(request.GetExchangeSubject()).IsAdmin {
|
||||||
|
return errors.New("user doesn't have impersonation permission")
|
||||||
|
}
|
||||||
|
|
||||||
|
allowedScopes := make([]string, 0)
|
||||||
|
for _, scope := range request.GetScopes() {
|
||||||
|
if scope == oidc.ScopeAddress {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(scope, CustomScopeImpersonatePrefix) {
|
||||||
|
subject := strings.TrimPrefix(scope, CustomScopeImpersonatePrefix)
|
||||||
|
request.SetSubject(subject)
|
||||||
|
}
|
||||||
|
|
||||||
|
allowedScopes = append(allowedScopes, scope)
|
||||||
|
}
|
||||||
|
|
||||||
|
request.SetCurrentScopes(allowedScopes)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateTokenExchangeRequest implements the op.TokenExchangeStorage interface
|
||||||
|
// Common use case is to store request for audit purposes. For this example we skip the storing.
|
||||||
|
func (s *Storage) CreateTokenExchangeRequest(ctx context.Context, request op.TokenExchangeRequest) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPrivateClaimsFromScopesForTokenExchange implements the op.TokenExchangeStorage interface
|
||||||
|
// it will be called for the creation of an exchanged JWT access token to assert claims for custom scopes
|
||||||
|
// plus adding token exchange specific claims related to delegation or impersonation
|
||||||
|
func (s *Storage) GetPrivateClaimsFromTokenExchangeRequest(ctx context.Context, request op.TokenExchangeRequest) (claims map[string]interface{}, err error) {
|
||||||
|
claims, err = s.getPrivateClaimsFromScopes(ctx, "", request.GetClientID(), request.GetScopes())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range s.getTokenExchangeClaims(ctx, request) {
|
||||||
|
claims = appendClaim(claims, k, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
return claims, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetUserinfoFromScopesForTokenExchange implements the op.TokenExchangeStorage interface
|
||||||
|
// it will be called for the creation of an id_token - we are using the same private function as for other flows,
|
||||||
|
// plus adding token exchange specific claims related to delegation or impersonation
|
||||||
|
func (s *Storage) SetUserinfoFromTokenExchangeRequest(ctx context.Context, userinfo *oidc.UserInfo, request op.TokenExchangeRequest) error {
|
||||||
|
err := s.setUserinfo(ctx, userinfo, request.GetSubject(), request.GetClientID(), request.GetScopes())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range s.getTokenExchangeClaims(ctx, request) {
|
||||||
|
userinfo.AppendClaims(k, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Storage) getTokenExchangeClaims(ctx context.Context, request op.TokenExchangeRequest) (claims map[string]interface{}) {
|
||||||
|
for _, scope := range request.GetScopes() {
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(scope, CustomScopeImpersonatePrefix) && request.GetExchangeActor() == "":
|
||||||
|
// Set actor subject claim for impersonation flow
|
||||||
|
claims = appendClaim(claims, "act", map[string]interface{}{
|
||||||
|
"sub": request.GetExchangeSubject(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set actor subject claim for delegation flow
|
||||||
|
// if request.GetExchangeActor() != "" {
|
||||||
|
// claims = appendClaim(claims, "act", map[string]interface{}{
|
||||||
|
// "sub": request.GetExchangeActor(),
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
|
||||||
|
return claims
|
||||||
|
}
|
||||||
|
|
||||||
// getInfoFromRequest returns the clientID, authTime and amr depending on the op.TokenRequest type / implementation
|
// getInfoFromRequest returns the clientID, authTime and amr depending on the op.TokenRequest type / implementation
|
||||||
func getInfoFromRequest(req op.TokenRequest) (clientID string, authTime time.Time, amr []string) {
|
func getInfoFromRequest(req op.TokenRequest) (clientID string, authTime time.Time, amr []string) {
|
||||||
authReq, ok := req.(*AuthRequest) // Code Flow (with scope offline_access)
|
authReq, ok := req.(*AuthRequest) // Code Flow (with scope offline_access)
|
||||||
|
@ -588,3 +776,126 @@ func appendClaim(claims map[string]interface{}, claim string, value interface{})
|
||||||
claims[claim] = value
|
claims[claim] = value
|
||||||
return claims
|
return claims
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type deviceAuthorizationEntry struct {
|
||||||
|
deviceCode string
|
||||||
|
userCode string
|
||||||
|
state *op.DeviceAuthorizationState
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Storage) StoreDeviceAuthorization(ctx context.Context, clientID, deviceCode, userCode string, expires time.Time, scopes []string) error {
|
||||||
|
s.lock.Lock()
|
||||||
|
defer s.lock.Unlock()
|
||||||
|
|
||||||
|
if _, ok := s.clients[clientID]; !ok {
|
||||||
|
return errors.New("client not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := s.userCodes[userCode]; ok {
|
||||||
|
return op.ErrDuplicateUserCode
|
||||||
|
}
|
||||||
|
|
||||||
|
s.deviceCodes[deviceCode] = deviceAuthorizationEntry{
|
||||||
|
deviceCode: deviceCode,
|
||||||
|
userCode: userCode,
|
||||||
|
state: &op.DeviceAuthorizationState{
|
||||||
|
ClientID: clientID,
|
||||||
|
Scopes: scopes,
|
||||||
|
Expires: expires,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
s.userCodes[userCode] = deviceCode
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Storage) GetDeviceAuthorizatonState(ctx context.Context, clientID, deviceCode string) (*op.DeviceAuthorizationState, error) {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return nil, ctx.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
s.lock.Lock()
|
||||||
|
defer s.lock.Unlock()
|
||||||
|
|
||||||
|
entry, ok := s.deviceCodes[deviceCode]
|
||||||
|
if !ok || entry.state.ClientID != clientID {
|
||||||
|
return nil, errors.New("device code not found for client") // is there a standard not found error in the framework?
|
||||||
|
}
|
||||||
|
|
||||||
|
return entry.state, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Storage) GetDeviceAuthorizationByUserCode(ctx context.Context, userCode string) (*op.DeviceAuthorizationState, error) {
|
||||||
|
s.lock.Lock()
|
||||||
|
defer s.lock.Unlock()
|
||||||
|
|
||||||
|
entry, ok := s.deviceCodes[s.userCodes[userCode]]
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("user code not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
return entry.state, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Storage) CompleteDeviceAuthorization(ctx context.Context, userCode, subject string) error {
|
||||||
|
s.lock.Lock()
|
||||||
|
defer s.lock.Unlock()
|
||||||
|
|
||||||
|
entry, ok := s.deviceCodes[s.userCodes[userCode]]
|
||||||
|
if !ok {
|
||||||
|
return errors.New("user code not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
entry.state.Subject = subject
|
||||||
|
entry.state.Done = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Storage) DenyDeviceAuthorization(ctx context.Context, userCode string) error {
|
||||||
|
s.lock.Lock()
|
||||||
|
defer s.lock.Unlock()
|
||||||
|
|
||||||
|
s.deviceCodes[s.userCodes[userCode]].state.Denied = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthRequestDone is used by testing and is not required to implement op.Storage
|
||||||
|
func (s *Storage) AuthRequestDone(id string) error {
|
||||||
|
s.lock.Lock()
|
||||||
|
defer s.lock.Unlock()
|
||||||
|
|
||||||
|
if req, ok := s.authRequests[id]; ok {
|
||||||
|
req.done = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.New("request not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Storage) ClientCredentials(ctx context.Context, clientID, clientSecret string) (op.Client, error) {
|
||||||
|
s.lock.Lock()
|
||||||
|
defer s.lock.Unlock()
|
||||||
|
|
||||||
|
client, ok := s.serviceUsers[clientID]
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("wrong service user or password")
|
||||||
|
}
|
||||||
|
if client.secret != clientSecret {
|
||||||
|
return nil, errors.New("wrong service user or password")
|
||||||
|
}
|
||||||
|
|
||||||
|
return client, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Storage) ClientCredentialsTokenRequest(ctx context.Context, clientID string, scopes []string) (op.TokenRequest, error) {
|
||||||
|
client, ok := s.serviceUsers[clientID]
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("wrong service user or password")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &oidc.JWTTokenRequest{
|
||||||
|
Subject: client.id,
|
||||||
|
Audience: []string{clientID},
|
||||||
|
Scopes: scopes,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
270
example/server/storage/storage_dynamic.go
Normal file
270
example/server/storage/storage_dynamic.go
Normal file
|
@ -0,0 +1,270 @@
|
||||||
|
package storage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gopkg.in/square/go-jose.v2"
|
||||||
|
|
||||||
|
"github.com/zitadel/oidc/v2/pkg/oidc"
|
||||||
|
"github.com/zitadel/oidc/v2/pkg/op"
|
||||||
|
)
|
||||||
|
|
||||||
|
type multiStorage struct {
|
||||||
|
issuers map[string]*Storage
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMultiStorage implements the op.Storage interface by wrapping multiple storage structs
|
||||||
|
// and selecting them by the calling issuer
|
||||||
|
func NewMultiStorage(issuers []string) *multiStorage {
|
||||||
|
s := make(map[string]*Storage)
|
||||||
|
for _, issuer := range issuers {
|
||||||
|
s[issuer] = NewStorage(NewUserStore(issuer))
|
||||||
|
}
|
||||||
|
return &multiStorage{issuers: s}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckUsernamePassword implements the `authenticate` interface of the login
|
||||||
|
func (s *multiStorage) CheckUsernamePassword(ctx context.Context, username, password, id string) error {
|
||||||
|
storage, err := s.storageFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return storage.CheckUsernamePassword(username, password, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateAuthRequest implements the op.Storage interface
|
||||||
|
// it will be called after parsing and validation of the authentication request
|
||||||
|
func (s *multiStorage) CreateAuthRequest(ctx context.Context, authReq *oidc.AuthRequest, userID string) (op.AuthRequest, error) {
|
||||||
|
storage, err := s.storageFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return storage.CreateAuthRequest(ctx, authReq, userID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthRequestByID implements the op.Storage interface
|
||||||
|
// it will be called after the Login UI redirects back to the OIDC endpoint
|
||||||
|
func (s *multiStorage) AuthRequestByID(ctx context.Context, id string) (op.AuthRequest, error) {
|
||||||
|
storage, err := s.storageFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return storage.AuthRequestByID(ctx, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthRequestByCode implements the op.Storage interface
|
||||||
|
// it will be called after parsing and validation of the token request (in an authorization code flow)
|
||||||
|
func (s *multiStorage) AuthRequestByCode(ctx context.Context, code string) (op.AuthRequest, error) {
|
||||||
|
storage, err := s.storageFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return storage.AuthRequestByCode(ctx, code)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveAuthCode implements the op.Storage interface
|
||||||
|
// it will be called after the authentication has been successful and before redirecting the user agent to the redirect_uri
|
||||||
|
// (in an authorization code flow)
|
||||||
|
func (s *multiStorage) SaveAuthCode(ctx context.Context, id string, code string) error {
|
||||||
|
storage, err := s.storageFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return storage.SaveAuthCode(ctx, id, code)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteAuthRequest implements the op.Storage interface
|
||||||
|
// it will be called after creating the token response (id and access tokens) for a valid
|
||||||
|
// - authentication request (in an implicit flow)
|
||||||
|
// - token request (in an authorization code flow)
|
||||||
|
func (s *multiStorage) DeleteAuthRequest(ctx context.Context, id string) error {
|
||||||
|
storage, err := s.storageFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return storage.DeleteAuthRequest(ctx, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateAccessToken implements the op.Storage interface
|
||||||
|
// it will be called for all requests able to return an access token (Authorization Code Flow, Implicit Flow, JWT Profile, ...)
|
||||||
|
func (s *multiStorage) CreateAccessToken(ctx context.Context, request op.TokenRequest) (string, time.Time, error) {
|
||||||
|
storage, err := s.storageFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return "", time.Time{}, err
|
||||||
|
}
|
||||||
|
return storage.CreateAccessToken(ctx, request)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateAccessAndRefreshTokens implements the op.Storage interface
|
||||||
|
// it will be called for all requests able to return an access and refresh token (Authorization Code Flow, Refresh Token Request)
|
||||||
|
func (s *multiStorage) CreateAccessAndRefreshTokens(ctx context.Context, request op.TokenRequest, currentRefreshToken string) (accessTokenID string, newRefreshToken string, expiration time.Time, err error) {
|
||||||
|
storage, err := s.storageFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", time.Time{}, err
|
||||||
|
}
|
||||||
|
return storage.CreateAccessAndRefreshTokens(ctx, request, currentRefreshToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TokenRequestByRefreshToken implements the op.Storage interface
|
||||||
|
// it will be called after parsing and validation of the refresh token request
|
||||||
|
func (s *multiStorage) TokenRequestByRefreshToken(ctx context.Context, refreshToken string) (op.RefreshTokenRequest, error) {
|
||||||
|
storage, err := s.storageFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return storage.TokenRequestByRefreshToken(ctx, refreshToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TerminateSession implements the op.Storage interface
|
||||||
|
// it will be called after the user signed out, therefore the access and refresh token of the user of this client must be removed
|
||||||
|
func (s *multiStorage) TerminateSession(ctx context.Context, userID string, clientID string) error {
|
||||||
|
storage, err := s.storageFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return storage.TerminateSession(ctx, userID, clientID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRefreshTokenInfo looks up a refresh token and returns the token id and user id.
|
||||||
|
// If given something that is not a refresh token, it must return error.
|
||||||
|
func (s *multiStorage) GetRefreshTokenInfo(ctx context.Context, clientID string, token string) (userID string, tokenID string, err error) {
|
||||||
|
storage, err := s.storageFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
return storage.GetRefreshTokenInfo(ctx, clientID, token)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RevokeToken implements the op.Storage interface
|
||||||
|
// it will be called after parsing and validation of the token revocation request
|
||||||
|
func (s *multiStorage) RevokeToken(ctx context.Context, token string, userID string, clientID string) *oidc.Error {
|
||||||
|
storage, err := s.storageFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return storage.RevokeToken(ctx, token, userID, clientID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SigningKey implements the op.Storage interface
|
||||||
|
// it will be called when creating the OpenID Provider
|
||||||
|
func (s *multiStorage) SigningKey(ctx context.Context) (op.SigningKey, error) {
|
||||||
|
storage, err := s.storageFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return storage.SigningKey(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SignatureAlgorithms implements the op.Storage interface
|
||||||
|
// it will be called to get the sign
|
||||||
|
func (s *multiStorage) SignatureAlgorithms(ctx context.Context) ([]jose.SignatureAlgorithm, error) {
|
||||||
|
storage, err := s.storageFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return storage.SignatureAlgorithms(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeySet implements the op.Storage interface
|
||||||
|
// it will be called to get the current (public) keys, among others for the keys_endpoint or for validating access_tokens on the userinfo_endpoint, ...
|
||||||
|
func (s *multiStorage) KeySet(ctx context.Context) ([]op.Key, error) {
|
||||||
|
storage, err := s.storageFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return storage.KeySet(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetClientByClientID implements the op.Storage interface
|
||||||
|
// it will be called whenever information (type, redirect_uris, ...) about the client behind the client_id is needed
|
||||||
|
func (s *multiStorage) GetClientByClientID(ctx context.Context, clientID string) (op.Client, error) {
|
||||||
|
storage, err := s.storageFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return storage.GetClientByClientID(ctx, clientID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthorizeClientIDSecret implements the op.Storage interface
|
||||||
|
// it will be called for validating the client_id, client_secret on token or introspection requests
|
||||||
|
func (s *multiStorage) AuthorizeClientIDSecret(ctx context.Context, clientID, clientSecret string) error {
|
||||||
|
storage, err := s.storageFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return storage.AuthorizeClientIDSecret(ctx, clientID, clientSecret)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetUserinfoFromScopes implements the op.Storage interface
|
||||||
|
// it will be called for the creation of an id_token, so we'll just pass it to the private function without any further check
|
||||||
|
func (s *multiStorage) SetUserinfoFromScopes(ctx context.Context, userinfo *oidc.UserInfo, userID, clientID string, scopes []string) error {
|
||||||
|
storage, err := s.storageFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return storage.SetUserinfoFromScopes(ctx, userinfo, userID, clientID, scopes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetUserinfoFromToken implements the op.Storage interface
|
||||||
|
// it will be called for the userinfo endpoint, so we read the token and pass the information from that to the private function
|
||||||
|
func (s *multiStorage) SetUserinfoFromToken(ctx context.Context, userinfo *oidc.UserInfo, tokenID, subject, origin string) error {
|
||||||
|
storage, err := s.storageFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return storage.SetUserinfoFromToken(ctx, userinfo, tokenID, subject, origin)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetIntrospectionFromToken implements the op.Storage interface
|
||||||
|
// it will be called for the introspection endpoint, so we read the token and pass the information from that to the private function
|
||||||
|
func (s *multiStorage) SetIntrospectionFromToken(ctx context.Context, introspection *oidc.IntrospectionResponse, tokenID, subject, clientID string) error {
|
||||||
|
storage, err := s.storageFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return storage.SetIntrospectionFromToken(ctx, introspection, tokenID, subject, clientID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPrivateClaimsFromScopes implements the op.Storage interface
|
||||||
|
// it will be called for the creation of a JWT access token to assert claims for custom scopes
|
||||||
|
func (s *multiStorage) GetPrivateClaimsFromScopes(ctx context.Context, userID, clientID string, scopes []string) (claims map[string]interface{}, err error) {
|
||||||
|
storage, err := s.storageFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return storage.GetPrivateClaimsFromScopes(ctx, userID, clientID, scopes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetKeyByIDAndClientID implements the op.Storage interface
|
||||||
|
// it will be called to validate the signatures of a JWT (JWT Profile Grant and Authentication)
|
||||||
|
func (s *multiStorage) GetKeyByIDAndClientID(ctx context.Context, keyID, userID string) (*jose.JSONWebKey, error) {
|
||||||
|
storage, err := s.storageFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return storage.GetKeyByIDAndClientID(ctx, keyID, userID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateJWTProfileScopes implements the op.Storage interface
|
||||||
|
// it will be called to validate the scopes of a JWT Profile Authorization Grant request
|
||||||
|
func (s *multiStorage) ValidateJWTProfileScopes(ctx context.Context, userID string, scopes []string) ([]string, error) {
|
||||||
|
storage, err := s.storageFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return storage.ValidateJWTProfileScopes(ctx, userID, scopes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Health implements the op.Storage interface
|
||||||
|
func (s *multiStorage) Health(ctx context.Context) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *multiStorage) storageFromContext(ctx context.Context) (*Storage, *oidc.Error) {
|
||||||
|
storage, ok := s.issuers[op.IssuerFromContext(ctx)]
|
||||||
|
if !ok {
|
||||||
|
return nil, oidc.ErrInvalidRequest().WithDescription("invalid issuer")
|
||||||
|
}
|
||||||
|
return storage, nil
|
||||||
|
}
|
|
@ -2,6 +2,7 @@ package storage
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"golang.org/x/text/language"
|
"golang.org/x/text/language"
|
||||||
)
|
)
|
||||||
|
@ -17,6 +18,7 @@ type User struct {
|
||||||
Phone string
|
Phone string
|
||||||
PhoneVerified bool
|
PhoneVerified bool
|
||||||
PreferredLanguage language.Tag
|
PreferredLanguage language.Tag
|
||||||
|
IsAdmin bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type Service struct {
|
type Service struct {
|
||||||
|
@ -33,12 +35,13 @@ type userStore struct {
|
||||||
users map[string]*User
|
users map[string]*User
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewUserStore() UserStore {
|
func NewUserStore(issuer string) UserStore {
|
||||||
|
hostname := strings.Split(strings.Split(issuer, "://")[1], ":")[0]
|
||||||
return userStore{
|
return userStore{
|
||||||
users: map[string]*User{
|
users: map[string]*User{
|
||||||
"id1": {
|
"id1": {
|
||||||
ID: "id1",
|
ID: "id1",
|
||||||
Username: "test-user",
|
Username: "test-user@" + hostname,
|
||||||
Password: "verysecure",
|
Password: "verysecure",
|
||||||
FirstName: "Test",
|
FirstName: "Test",
|
||||||
LastName: "User",
|
LastName: "User",
|
||||||
|
@ -47,6 +50,20 @@ func NewUserStore() UserStore {
|
||||||
Phone: "",
|
Phone: "",
|
||||||
PhoneVerified: false,
|
PhoneVerified: false,
|
||||||
PreferredLanguage: language.German,
|
PreferredLanguage: language.German,
|
||||||
|
IsAdmin: true,
|
||||||
|
},
|
||||||
|
"id2": {
|
||||||
|
ID: "id2",
|
||||||
|
Username: "test-user2",
|
||||||
|
Password: "verysecure",
|
||||||
|
FirstName: "Test",
|
||||||
|
LastName: "User2",
|
||||||
|
Email: "test-user2@zitadel.ch",
|
||||||
|
EmailVerified: true,
|
||||||
|
Phone: "",
|
||||||
|
PhoneVerified: false,
|
||||||
|
PreferredLanguage: language.German,
|
||||||
|
IsAdmin: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
28
go.mod
28
go.mod
|
@ -1,23 +1,35 @@
|
||||||
module github.com/zitadel/oidc
|
module github.com/zitadel/oidc/v2
|
||||||
|
|
||||||
go 1.16
|
go 1.18
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/golang/mock v1.6.0
|
github.com/golang/mock v1.6.0
|
||||||
github.com/google/go-cmp v0.5.2 // indirect
|
|
||||||
github.com/google/go-github/v31 v31.0.0
|
github.com/google/go-github/v31 v31.0.0
|
||||||
github.com/google/uuid v1.3.0
|
github.com/google/uuid v1.3.0
|
||||||
github.com/gorilla/mux v1.8.0
|
github.com/gorilla/mux v1.8.0
|
||||||
github.com/gorilla/schema v1.2.0
|
github.com/gorilla/schema v1.2.0
|
||||||
github.com/gorilla/securecookie v1.1.1
|
github.com/gorilla/securecookie v1.1.1
|
||||||
github.com/jeremija/gosubmit v0.2.7
|
github.com/jeremija/gosubmit v0.2.7
|
||||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
|
github.com/muhlemmer/gu v0.3.1
|
||||||
github.com/rs/cors v1.8.3
|
github.com/rs/cors v1.8.3
|
||||||
github.com/sirupsen/logrus v1.9.0
|
github.com/sirupsen/logrus v1.9.0
|
||||||
github.com/stretchr/testify v1.8.2
|
github.com/stretchr/testify v1.8.2
|
||||||
github.com/zitadel/logging v0.3.4
|
golang.org/x/oauth2 v0.6.0
|
||||||
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43
|
golang.org/x/text v0.8.0
|
||||||
golang.org/x/text v0.7.0
|
|
||||||
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b // indirect
|
|
||||||
gopkg.in/square/go-jose.v2 v2.6.0
|
gopkg.in/square/go-jose.v2 v2.6.0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/golang/protobuf v1.5.3 // indirect
|
||||||
|
github.com/google/go-cmp v0.5.9 // indirect
|
||||||
|
github.com/google/go-querystring v1.1.0 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
golang.org/x/crypto v0.7.0 // indirect
|
||||||
|
golang.org/x/net v0.8.0 // indirect
|
||||||
|
golang.org/x/sys v0.6.0 // indirect
|
||||||
|
google.golang.org/appengine v1.6.7 // indirect
|
||||||
|
google.golang.org/protobuf v1.29.0 // indirect
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
)
|
||||||
|
|
382
go.sum
382
go.sum
|
@ -1,144 +1,50 @@
|
||||||
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=
|
|
||||||
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
|
|
||||||
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
|
|
||||||
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
|
|
||||||
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
|
|
||||||
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
|
|
||||||
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
|
|
||||||
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
|
|
||||||
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
|
|
||||||
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
|
|
||||||
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
|
|
||||||
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
|
|
||||||
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
|
|
||||||
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
|
||||||
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
|
|
||||||
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
|
|
||||||
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
|
|
||||||
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
|
|
||||||
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
|
|
||||||
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
|
|
||||||
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
|
|
||||||
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
|
|
||||||
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
|
|
||||||
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
|
|
||||||
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
|
|
||||||
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
|
|
||||||
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
|
|
||||||
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
|
|
||||||
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
|
|
||||||
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
|
|
||||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
|
||||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
|
||||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
|
||||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
|
||||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
|
||||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
|
||||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
|
||||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
|
||||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
|
||||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
|
||||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
|
||||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
|
||||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
|
||||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
|
||||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
|
||||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
|
||||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
|
||||||
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
|
||||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
|
||||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
|
||||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
|
||||||
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
|
|
||||||
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
|
||||||
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
|
||||||
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
|
||||||
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
|
|
||||||
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
|
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
|
||||||
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
|
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
|
||||||
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.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||||
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||||
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
|
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
|
||||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
|
||||||
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.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
|
||||||
github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=
|
|
||||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
|
||||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
|
||||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
|
||||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
|
||||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
|
||||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
|
||||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
|
||||||
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
|
||||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
|
||||||
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
|
||||||
github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM=
|
|
||||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||||
|
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/go-github/v31 v31.0.0 h1:JJUxlP9lFK+ziXKimTCprajMApV1ecWD4NB6CCb0plo=
|
github.com/google/go-github/v31 v31.0.0 h1:JJUxlP9lFK+ziXKimTCprajMApV1ecWD4NB6CCb0plo=
|
||||||
github.com/google/go-github/v31 v31.0.0/go.mod h1:NQPZol8/1sMoWYGN2yaALIBytu17gAWfhbweiEed3pM=
|
github.com/google/go-github/v31 v31.0.0/go.mod h1:NQPZol8/1sMoWYGN2yaALIBytu17gAWfhbweiEed3pM=
|
||||||
github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
|
|
||||||
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
||||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
|
||||||
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
|
||||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
|
||||||
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
|
||||||
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
|
||||||
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
|
||||||
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
|
||||||
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
|
||||||
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
|
||||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
|
||||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.3.0/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/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
||||||
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||||
github.com/gorilla/schema v1.2.0 h1:YufUaxZYCKGFuAq3c96BOhjgd5nmXiOY9NGzF247Tsc=
|
github.com/gorilla/schema v1.2.0 h1:YufUaxZYCKGFuAq3c96BOhjgd5nmXiOY9NGzF247Tsc=
|
||||||
github.com/gorilla/schema v1.2.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU=
|
github.com/gorilla/schema v1.2.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU=
|
||||||
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
|
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
|
||||||
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
|
||||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
|
||||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
|
||||||
github.com/jeremija/gosubmit v0.2.7 h1:At0OhGCFGPXyjPYAsCchoBUhE099pcBXmsb4iZqROIc=
|
github.com/jeremija/gosubmit v0.2.7 h1:At0OhGCFGPXyjPYAsCchoBUhE099pcBXmsb4iZqROIc=
|
||||||
github.com/jeremija/gosubmit v0.2.7/go.mod h1:Ui+HS073lCFREXBbdfrJzMB57OI/bdxTiLtrDHHhFPI=
|
github.com/jeremija/gosubmit v0.2.7/go.mod h1:Ui+HS073lCFREXBbdfrJzMB57OI/bdxTiLtrDHHhFPI=
|
||||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
|
||||||
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
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/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
github.com/muhlemmer/gu v0.3.1 h1:7EAqmFrW7n3hETvuAdmFmn4hS8W+z3LgKtrnow+YzNM=
|
||||||
|
github.com/muhlemmer/gu v0.3.1/go.mod h1:YHtHR+gxM+bKEIIs7Hmi9sPT3ZDUvTN/i88wQpZkrdM=
|
||||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
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/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
|
||||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
|
||||||
github.com/rs/cors v1.8.3 h1:O+qNyWn7Z+F9M0ILBHgMVPuB1xTOucVd5gtaYyXBpRo=
|
github.com/rs/cors v1.8.3 h1:O+qNyWn7Z+F9M0ILBHgMVPuB1xTOucVd5gtaYyXBpRo=
|
||||||
github.com/rs/cors v1.8.3/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
|
github.com/rs/cors v1.8.3/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
|
||||||
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
|
||||||
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
|
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
|
||||||
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
|
@ -148,140 +54,34 @@ github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
|
||||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
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.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
|
||||||
github.com/zitadel/logging v0.3.4 h1:9hZsTjMMTE3X2LUi0xcF9Q9EdLo+FAezeu52ireBbHM=
|
|
||||||
github.com/zitadel/logging v0.3.4/go.mod h1:aPpLQhE+v6ocNK0TWrBrd363hZ95KcI17Q1ixAQwZF0=
|
|
||||||
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.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
|
||||||
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
|
||||||
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
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-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg=
|
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
|
||||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/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-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/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
|
||||||
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
|
||||||
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
|
||||||
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
|
||||||
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
|
|
||||||
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=
|
|
||||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
|
||||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
|
||||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
|
||||||
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
|
||||||
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
|
||||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
|
||||||
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
|
|
||||||
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
|
||||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/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.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
|
||||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
|
||||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
|
||||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
|
||||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/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-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
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-20190603091049-60506f45cf65/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-20190620200207-3b0461eec859/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-20191209160850-c0dbc17a3553/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-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/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-20200301022130-244492dfa37a/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-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
|
||||||
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
|
||||||
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
|
||||||
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
|
||||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
|
||||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
|
||||||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
|
||||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b h1:PxfKdU9lEEDYjdIzOtC4qFWgkU2rGHdKlKowJSMN9h0=
|
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
|
||||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.6.0 h1:Lh8GPgSKBfWSwFvtuWOfeI3aAAnbXTSutYxJiOJFgIw=
|
||||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw=
|
||||||
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
|
||||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
|
||||||
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43 h1:ld7aEMNHoBnnDAX15v1T6z31v8HwR2A9FYOuAhWqkwc=
|
|
||||||
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
|
||||||
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-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-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/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-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/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-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=
|
|
||||||
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/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-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20220207234003-57398862261d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s=
|
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
|
||||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
|
||||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
|
||||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
@ -290,149 +90,27 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
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/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
|
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
|
||||||
|
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/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-20190114222345-bf090417da8b/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=
|
|
||||||
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
|
||||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
|
||||||
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-20190621195816-6e04913cbbac/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-20190816200558-6889da9d5479/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-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
|
||||||
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
|
||||||
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/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-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
|
||||||
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
|
||||||
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
|
||||||
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
|
||||||
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
|
||||||
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/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-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
|
||||||
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
|
||||||
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
|
||||||
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
|
||||||
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
|
||||||
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
|
||||||
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
|
||||||
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
|
|
||||||
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
|
||||||
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
|
||||||
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
|
||||||
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
|
||||||
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
|
||||||
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
|
||||||
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
|
||||||
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
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=
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
|
||||||
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
|
||||||
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
|
||||||
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
|
||||||
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
|
||||||
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
|
||||||
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
|
||||||
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
|
||||||
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
|
||||||
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
|
||||||
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
|
||||||
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
|
||||||
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
|
||||||
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
|
||||||
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
|
|
||||||
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
|
|
||||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
|
||||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||||
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||||
google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc=
|
google.golang.org/protobuf v1.29.0 h1:44S3JjaKmLEE4YIkjzexaP+NzZsudE3Zin5Njn/pYX0=
|
||||||
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
google.golang.org/protobuf v1.29.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||||
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=
|
|
||||||
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
|
||||||
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
|
||||||
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=
|
|
||||||
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
|
||||||
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
|
||||||
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
|
||||||
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
|
||||||
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
|
||||||
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
|
||||||
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
|
|
||||||
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
|
||||||
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
|
||||||
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
|
||||||
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
|
||||||
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
|
||||||
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
|
||||||
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
|
||||||
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
|
||||||
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
|
|
||||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
|
||||||
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
|
|
||||||
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
|
||||||
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
|
||||||
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
|
||||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
|
||||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
|
||||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
|
||||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
|
||||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
|
||||||
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
|
||||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
|
||||||
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
|
||||||
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
|
|
||||||
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
|
|
||||||
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
|
||||||
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
|
||||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
|
||||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
|
||||||
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.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
|
||||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
|
||||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
|
||||||
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
|
|
||||||
google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=
|
|
||||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b h1:QRR6H1YWRnHb4Y/HeNFCTJLFVxaq6wH4YuVdsUOr75U=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
||||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
|
||||||
gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI=
|
gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI=
|
||||||
gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
|
gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
||||||
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
|
|
||||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
|
||||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
|
||||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
|
||||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
|
||||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
|
||||||
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
|
||||||
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
|
||||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
|
||||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
|
||||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
|
||||||
|
|
58
internal/testutil/gen/gen.go
Normal file
58
internal/testutil/gen/gen.go
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
// Package gen allows generating of example tokens and claims.
|
||||||
|
//
|
||||||
|
// go run ./internal/testutil/gen
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
tu "github.com/zitadel/oidc/v2/internal/testutil"
|
||||||
|
"github.com/zitadel/oidc/v2/pkg/oidc"
|
||||||
|
)
|
||||||
|
|
||||||
|
var custom = map[string]any{
|
||||||
|
"foo": "Hello, World!",
|
||||||
|
"bar": struct {
|
||||||
|
Count int `json:"count,omitempty"`
|
||||||
|
Tags []string `json:"tags,omitempty"`
|
||||||
|
}{
|
||||||
|
Count: 22,
|
||||||
|
Tags: []string{"some", "tags"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
enc := json.NewEncoder(os.Stdout)
|
||||||
|
enc.SetIndent("", " ")
|
||||||
|
|
||||||
|
accessToken, atClaims := tu.NewAccessTokenCustom(
|
||||||
|
tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience,
|
||||||
|
tu.ValidExpiration.AddDate(99, 0, 0), tu.ValidJWTID,
|
||||||
|
tu.ValidClientID, tu.ValidSkew, custom,
|
||||||
|
)
|
||||||
|
atHash, err := oidc.ClaimHash(accessToken, tu.SignatureAlgorithm)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
idToken, idClaims := tu.NewIDTokenCustom(
|
||||||
|
tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience,
|
||||||
|
tu.ValidExpiration.AddDate(99, 0, 0), tu.ValidAuthTime,
|
||||||
|
tu.ValidNonce, tu.ValidACR, tu.ValidAMR, tu.ValidClientID,
|
||||||
|
tu.ValidSkew, atHash, custom,
|
||||||
|
)
|
||||||
|
|
||||||
|
fmt.Println("access token claims:")
|
||||||
|
if err := enc.Encode(atClaims); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
fmt.Printf("access token:\n%s\n", accessToken)
|
||||||
|
|
||||||
|
fmt.Println("ID token claims:")
|
||||||
|
if err := enc.Encode(idClaims); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
fmt.Printf("ID token:\n%s\n", idToken)
|
||||||
|
}
|
146
internal/testutil/token.go
Normal file
146
internal/testutil/token.go
Normal file
|
@ -0,0 +1,146 @@
|
||||||
|
// Package testuril helps setting up required data for testing,
|
||||||
|
// such as tokens, claims and verifiers.
|
||||||
|
package testutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/zitadel/oidc/v2/pkg/oidc"
|
||||||
|
"gopkg.in/square/go-jose.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// KeySet implements oidc.Keys
|
||||||
|
type KeySet struct{}
|
||||||
|
|
||||||
|
// VerifySignature implments op.KeySet.
|
||||||
|
func (KeySet) VerifySignature(ctx context.Context, jws *jose.JSONWebSignature) (payload []byte, err error) {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return jws.Verify(WebKey.Public())
|
||||||
|
}
|
||||||
|
|
||||||
|
// use a reproducible signing key
|
||||||
|
const webkeyJSON = `{"kty":"RSA","kid":"1","alg":"PS512","n":"x6JoG8t2Li68JSwPwnh51TvHYFf3z72tQ3wmJG3VosU6MdJF0gSTCIwflOJ38OWE6hYtN1WAeyBy2CYdnXd1QZzkK_apGK4M7hsNA9jCTg8NOZjLPL0ww1jp7313Skla7mbm90uNdg4TUNp2n_r-sCYywI-9cfSlhzLSksxKK_BRdzy6xW20daAcI-mErQXIcvdYIguunJk_uTb8kJedsWMcQ4Mb57QujUok2Z2YabWyb9Fi1_StixXJvd_WEu93SHNMORB0u6ymnO3aZJdATLdhtcP-qsVicQhffpqVazmZQPf7K-7n4I5vJE4g9XXzZ2dSKSp3Ewe_nna_2kvbCw","e":"AQAB","d":"sl3F_QeF2O-CxQegMRYpbL6Tfd47GM6VDxXOkn_cACmNvFPudB4ILPvdf830cjTv06Lq1WS8fcZZNgygK0A_cNc3-pvRK67e-KMMtuIlgU7rdwmwlN1Iw1Ee-w6z1ZjC-PzR4iQMCW28DmKS2I-OnV4TvH7xOe7nMmvTPrvujV__YKfUxvAWXJG7_wtaJBGplezn5nNsKG2Ot9h0mhMdYUgGC36wLxo3Q5d4m79EXQYdhm89EfxogwvMmHRes5PNpHRuDZRHGAI4RZi2KvgmqF07e1Qdq4TqbQnY5pCYrdjqvEFFjGC6jTE-ak_b21FcSVy-9aZHyf04U4g5-cIUEQ","p":"7AaicFryJCHRekdSkx8tfPxaSiyEuN8jhP9cLqs4rLkIbrSHmanPhjnLe-Tlh3icQ8hPoy6WC8ktLwsrzbfGIh4U_zgAfvtD1Y_lZM-YSWZsxqlrGiI5do11iVzzoy4a1XdkgOjHQz9y6J-uoA9jY8ILG7VaEZQnaYwWZV3cspk","q":"2Ide9hlwthXJQJYqI0mibM5BiGBxJ4CafPmF1DYNXggBCczZ6ERGReNTGM_AEhy5mvLXUH6uBSOJlfHTYzx49C1GgIO3hEWVEGAKAytVRL6RfAkVSOXMQUp-HjXKpGg_Nx1SJxQf3rulbW8HXO4KqIlloyIXpPQSK7jB8A4hJUM","dp":"1nmc6F4sRNsaQHRJO_mL21RxM4_KtzfFThjCCoJ6iLHHUNnpkp_1PTKNjrLMRFM8JHgErfMqU-FmlqYfEtvZRq1xRQ39nWX0GT-eIwJljuVtGQVglqnc77bRxJXbqz-9EJdik6VzVM92Op7IDxiMp1zvvSkJhInNWqL6wvgNEZk","dq":"dlHizlAwiw90ndpwxD-khhhfLwqkSpW31br0KnYu78cn6hcKrCVC0UXbTp-XsU4JDmbMyauvpBc7Q7iVbpDI94UWFXvkeF8diYkxb3HqclpAXasI-oC4EKWILTHvvc9JW_Clx7zzfV7Ekvws5dcd8-LAq1gh232TwFiBgY_3BMk","qi":"E1k_9W3odXgcmIP2PCJztE7hB7jeuAL1ElAY88VJBBPY670uwOEjKL2VfQuz9q9IjzLAvcgf7vS9blw2RHP_XqHqSOlJWGwvMQTF0Q8zLknCgKt8q7HQQNWIJcBZ8qdUVn02-qf4E3tgZ3JHaHNs8imA_L-__WoUmzC4z5jH_lM"}`
|
||||||
|
|
||||||
|
const SignatureAlgorithm = jose.RS256
|
||||||
|
|
||||||
|
var (
|
||||||
|
WebKey jose.JSONWebKey
|
||||||
|
Signer jose.Signer
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
err := json.Unmarshal([]byte(webkeyJSON), &WebKey)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
Signer, err = jose.NewSigner(jose.SigningKey{Algorithm: SignatureAlgorithm, Key: WebKey}, nil)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func signEncodeTokenClaims(claims any) string {
|
||||||
|
payload, err := json.Marshal(claims)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
object, err := Signer.Sign(payload)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
token, err := object.CompactSerialize()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return token
|
||||||
|
}
|
||||||
|
|
||||||
|
func claimsMap(claims any) map[string]any {
|
||||||
|
data, err := json.Marshal(claims)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
dst := make(map[string]any)
|
||||||
|
if err = json.Unmarshal(data, &dst); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return dst
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewIDTokenCustom(issuer, subject string, audience []string, expiration, authTime time.Time, nonce string, acr string, amr []string, clientID string, skew time.Duration, atHash string, custom map[string]any) (string, *oidc.IDTokenClaims) {
|
||||||
|
claims := oidc.NewIDTokenClaims(issuer, subject, audience, expiration, authTime, nonce, acr, amr, clientID, skew)
|
||||||
|
claims.AccessTokenHash = atHash
|
||||||
|
claims.Claims = custom
|
||||||
|
token := signEncodeTokenClaims(claims)
|
||||||
|
|
||||||
|
// set this so that assertion in tests will work
|
||||||
|
claims.SignatureAlg = SignatureAlgorithm
|
||||||
|
claims.Claims = claimsMap(claims)
|
||||||
|
return token, claims
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewIDToken creates a new IDTokenClaims with passed data and returns a signed token and claims.
|
||||||
|
func NewIDToken(issuer, subject string, audience []string, expiration, authTime time.Time, nonce string, acr string, amr []string, clientID string, skew time.Duration, atHash string) (string, *oidc.IDTokenClaims) {
|
||||||
|
return NewIDTokenCustom(issuer, subject, audience, expiration, authTime, nonce, acr, amr, clientID, skew, atHash, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAccessTokenCustom(issuer, subject string, audience []string, expiration time.Time, jwtid, clientID string, skew time.Duration, custom map[string]any) (string, *oidc.AccessTokenClaims) {
|
||||||
|
claims := oidc.NewAccessTokenClaims(issuer, subject, audience, expiration, jwtid, clientID, skew)
|
||||||
|
claims.Claims = custom
|
||||||
|
token := signEncodeTokenClaims(claims)
|
||||||
|
|
||||||
|
// set this so that assertion in tests will work
|
||||||
|
claims.SignatureAlg = SignatureAlgorithm
|
||||||
|
claims.Claims = claimsMap(claims)
|
||||||
|
return token, claims
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAcccessToken creates a new AccessTokenClaims with passed data and returns a signed token and claims.
|
||||||
|
func NewAccessToken(issuer, subject string, audience []string, expiration time.Time, jwtid, clientID string, skew time.Duration) (string, *oidc.AccessTokenClaims) {
|
||||||
|
return NewAccessTokenCustom(issuer, subject, audience, expiration, jwtid, clientID, skew, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
const InvalidSignatureToken = `eyJhbGciOiJQUzUxMiJ9.eyJpc3MiOiJsb2NhbC5jb20iLCJzdWIiOiJ0aW1AbG9jYWwuY29tIiwiYXVkIjpbInVuaXQiLCJ0ZXN0IiwiNTU1NjY2Il0sImV4cCI6MTY3Nzg0MDQzMSwiaWF0IjoxNjc3ODQwMzcwLCJhdXRoX3RpbWUiOjE2Nzc4NDAzMTAsIm5vbmNlIjoiMTIzNDUiLCJhY3IiOiJzb21ldGhpbmciLCJhbXIiOlsiZm9vIiwiYmFyIl0sImF6cCI6IjU1NTY2NiJ9.DtZmvVkuE4Hw48ijBMhRJbxEWCr_WEYuPQBMY73J9TP6MmfeNFkjVJf4nh4omjB9gVLnQ-xhEkNOe62FS5P0BB2VOxPuHZUj34dNspCgG3h98fGxyiMb5vlIYAHDF9T-w_LntlYItohv63MmdYR-hPpAqjXE7KOfErf-wUDGE9R3bfiQ4HpTdyFJB1nsToYrZ9lhP2mzjTCTs58ckZfQ28DFHn_lfHWpR4rJBgvLx7IH4rMrUayr09Ap-PxQLbv0lYMtmgG1z3JK8MXnuYR0UJdZnEIezOzUTlThhCXB-nvuAXYjYxZZTR0FtlgZUHhIpYK0V2abf_Q_Or36akNCUg`
|
||||||
|
|
||||||
|
// These variables always result in a valid token
|
||||||
|
var (
|
||||||
|
ValidIssuer = "local.com"
|
||||||
|
ValidSubject = "tim@local.com"
|
||||||
|
ValidAudience = []string{"unit", "test"}
|
||||||
|
ValidAuthTime = time.Now().Add(-time.Minute) // authtime is always 1 minute in the past
|
||||||
|
ValidExpiration = ValidAuthTime.Add(2 * time.Minute) // token is always 1 more minute available
|
||||||
|
ValidJWTID = "9876"
|
||||||
|
ValidNonce = "12345"
|
||||||
|
ValidACR = "something"
|
||||||
|
ValidAMR = []string{"foo", "bar"}
|
||||||
|
ValidClientID = "555666"
|
||||||
|
ValidSkew = time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
// ValidIDToken returns a token and claims that are in the token.
|
||||||
|
// It uses the Valid* global variables and the token will always
|
||||||
|
// pass verification.
|
||||||
|
func ValidIDToken() (string, *oidc.IDTokenClaims) {
|
||||||
|
return NewIDToken(ValidIssuer, ValidSubject, ValidAudience, ValidExpiration, ValidAuthTime, ValidNonce, ValidACR, ValidAMR, ValidClientID, ValidSkew, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidAccessToken returns a token and claims that are in the token.
|
||||||
|
// It uses the Valid* global variables and the token always passes
|
||||||
|
// verification within the same test run.
|
||||||
|
func ValidAccessToken() (string, *oidc.AccessTokenClaims) {
|
||||||
|
return NewAccessToken(ValidIssuer, ValidSubject, ValidAudience, ValidExpiration, ValidJWTID, ValidClientID, ValidSkew)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ACRVerify is a oidc.ACRVerifier func.
|
||||||
|
func ACRVerify(acr string) error {
|
||||||
|
if acr != ValidACR {
|
||||||
|
return errors.New("invalid acr")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -1,31 +1,25 @@
|
||||||
package client
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"reflect"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gorilla/schema"
|
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
"gopkg.in/square/go-jose.v2"
|
"gopkg.in/square/go-jose.v2"
|
||||||
|
|
||||||
"github.com/zitadel/oidc/pkg/crypto"
|
"github.com/zitadel/oidc/v2/pkg/crypto"
|
||||||
httphelper "github.com/zitadel/oidc/pkg/http"
|
httphelper "github.com/zitadel/oidc/v2/pkg/http"
|
||||||
"github.com/zitadel/oidc/pkg/oidc"
|
"github.com/zitadel/oidc/v2/pkg/oidc"
|
||||||
)
|
)
|
||||||
|
|
||||||
var Encoder = func() httphelper.Encoder {
|
var Encoder = httphelper.Encoder(oidc.NewEncoder())
|
||||||
e := schema.NewEncoder()
|
|
||||||
e.RegisterEncoder(oidc.SpaceDelimitedArray{}, func(value reflect.Value) string {
|
|
||||||
return value.Interface().(oidc.SpaceDelimitedArray).Encode()
|
|
||||||
})
|
|
||||||
return e
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Discover calls the discovery endpoint of the provided issuer and returns its configuration
|
// Discover calls the discovery endpoint of the provided issuer and returns its configuration
|
||||||
// It accepts an optional argument "wellknownUrl" which can be used to overide the dicovery endpoint url
|
// It accepts an optional argument "wellknownUrl" which can be used to overide the dicovery endpoint url
|
||||||
|
@ -90,6 +84,9 @@ func CallEndSessionEndpoint(request interface{}, authFn interface{}, caller EndS
|
||||||
return http.ErrUseLastResponse
|
return http.ErrUseLastResponse
|
||||||
}
|
}
|
||||||
resp, err := client.Do(req)
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
if resp.StatusCode < 200 || resp.StatusCode >= 400 {
|
if resp.StatusCode < 200 || resp.StatusCode >= 400 {
|
||||||
body, err := io.ReadAll(resp.Body)
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
@ -148,6 +145,18 @@ func CallRevokeEndpoint(request interface{}, authFn interface{}, caller RevokeCa
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func CallTokenExchangeEndpoint(request interface{}, authFn interface{}, caller TokenEndpointCaller) (resp *oidc.TokenExchangeResponse, err error) {
|
||||||
|
req, err := httphelper.FormRequest(caller.TokenEndpoint(), request, Encoder, authFn)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
tokenRes := new(oidc.TokenExchangeResponse)
|
||||||
|
if err := httphelper.HttpRequest(caller.HttpClient(), req, &tokenRes); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return tokenRes, nil
|
||||||
|
}
|
||||||
|
|
||||||
func NewSignerFromPrivateKeyByte(key []byte, keyID string) (jose.Signer, error) {
|
func NewSignerFromPrivateKeyByte(key []byte, keyID string) (jose.Signer, error) {
|
||||||
privateKey, err := crypto.BytesToPrivateKey(key)
|
privateKey, err := crypto.BytesToPrivateKey(key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -167,7 +176,98 @@ func SignedJWTProfileAssertion(clientID string, audience []string, expiration ti
|
||||||
Issuer: clientID,
|
Issuer: clientID,
|
||||||
Subject: clientID,
|
Subject: clientID,
|
||||||
Audience: audience,
|
Audience: audience,
|
||||||
ExpiresAt: oidc.Time(exp),
|
ExpiresAt: oidc.FromTime(exp),
|
||||||
IssuedAt: oidc.Time(iat),
|
IssuedAt: oidc.FromTime(iat),
|
||||||
}, signer)
|
}, signer)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DeviceAuthorizationCaller interface {
|
||||||
|
GetDeviceAuthorizationEndpoint() string
|
||||||
|
HttpClient() *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func CallDeviceAuthorizationEndpoint(request *oidc.ClientCredentialsRequest, caller DeviceAuthorizationCaller) (*oidc.DeviceAuthorizationResponse, error) {
|
||||||
|
req, err := httphelper.FormRequest(caller.GetDeviceAuthorizationEndpoint(), request, Encoder, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if request.ClientSecret != "" {
|
||||||
|
req.SetBasicAuth(request.ClientID, request.ClientSecret)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := new(oidc.DeviceAuthorizationResponse)
|
||||||
|
if err := httphelper.HttpRequest(caller.HttpClient(), req, &resp); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type DeviceAccessTokenRequest struct {
|
||||||
|
*oidc.ClientCredentialsRequest
|
||||||
|
oidc.DeviceAccessTokenRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
func CallDeviceAccessTokenEndpoint(ctx context.Context, request *DeviceAccessTokenRequest, caller TokenEndpointCaller) (*oidc.AccessTokenResponse, error) {
|
||||||
|
req, err := httphelper.FormRequest(caller.TokenEndpoint(), request, Encoder, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if request.ClientSecret != "" {
|
||||||
|
req.SetBasicAuth(request.ClientID, request.ClientSecret)
|
||||||
|
}
|
||||||
|
|
||||||
|
httpResp, err := caller.HttpClient().Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer httpResp.Body.Close()
|
||||||
|
|
||||||
|
resp := new(struct {
|
||||||
|
*oidc.AccessTokenResponse
|
||||||
|
*oidc.Error
|
||||||
|
})
|
||||||
|
if err = json.NewDecoder(httpResp.Body).Decode(resp); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if httpResp.StatusCode == http.StatusOK {
|
||||||
|
return resp.AccessTokenResponse, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, resp.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func PollDeviceAccessTokenEndpoint(ctx context.Context, interval time.Duration, request *DeviceAccessTokenRequest, caller TokenEndpointCaller) (*oidc.AccessTokenResponse, error) {
|
||||||
|
for {
|
||||||
|
timer := time.After(interval)
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil, ctx.Err()
|
||||||
|
case <-timer:
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(ctx, interval)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
resp, err := CallDeviceAccessTokenEndpoint(ctx, request, caller)
|
||||||
|
if err == nil {
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
if errors.Is(err, context.DeadlineExceeded) {
|
||||||
|
interval += 5 * time.Second
|
||||||
|
}
|
||||||
|
var target *oidc.Error
|
||||||
|
if !errors.As(err, &target) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
switch target.ErrorType {
|
||||||
|
case oidc.AuthorizationPending:
|
||||||
|
continue
|
||||||
|
case oidc.SlowDown:
|
||||||
|
interval += 5 * time.Second
|
||||||
|
continue
|
||||||
|
default:
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
package rp_test
|
package client_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
|
@ -15,34 +14,142 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/zitadel/oidc/example/server/exampleop"
|
|
||||||
"github.com/zitadel/oidc/example/server/storage"
|
|
||||||
|
|
||||||
"github.com/jeremija/gosubmit"
|
"github.com/jeremija/gosubmit"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"github.com/zitadel/oidc/pkg/client/rp"
|
|
||||||
httphelper "github.com/zitadel/oidc/pkg/http"
|
"github.com/zitadel/oidc/v2/example/server/exampleop"
|
||||||
"github.com/zitadel/oidc/pkg/oidc"
|
"github.com/zitadel/oidc/v2/example/server/storage"
|
||||||
|
"github.com/zitadel/oidc/v2/pkg/client/rp"
|
||||||
|
"github.com/zitadel/oidc/v2/pkg/client/rs"
|
||||||
|
"github.com/zitadel/oidc/v2/pkg/client/tokenexchange"
|
||||||
|
httphelper "github.com/zitadel/oidc/v2/pkg/http"
|
||||||
|
"github.com/zitadel/oidc/v2/pkg/oidc"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestRelyingPartySession(t *testing.T) {
|
func TestRelyingPartySession(t *testing.T) {
|
||||||
t.Log("------- start example OP ------")
|
t.Log("------- start example OP ------")
|
||||||
ctx := context.Background()
|
targetURL := "http://local-site"
|
||||||
exampleStorage := storage.NewStorage(storage.NewUserStore())
|
exampleStorage := storage.NewStorage(storage.NewUserStore(targetURL))
|
||||||
var dh deferredHandler
|
var dh deferredHandler
|
||||||
opServer := httptest.NewServer(&dh)
|
opServer := httptest.NewServer(&dh)
|
||||||
defer opServer.Close()
|
defer opServer.Close()
|
||||||
t.Logf("auth server at %s", opServer.URL)
|
t.Logf("auth server at %s", opServer.URL)
|
||||||
dh.Handler = exampleop.SetupServer(ctx, opServer.URL, exampleStorage)
|
dh.Handler = exampleop.SetupServer(opServer.URL, exampleStorage)
|
||||||
|
|
||||||
|
seed := rand.New(rand.NewSource(int64(os.Getpid()) + time.Now().UnixNano()))
|
||||||
|
clientID := t.Name() + "-" + strconv.FormatInt(seed.Int63(), 25)
|
||||||
|
|
||||||
|
t.Log("------- run authorization code flow ------")
|
||||||
|
provider, _, refreshToken, idToken := RunAuthorizationCodeFlow(t, opServer, clientID, "secret")
|
||||||
|
|
||||||
|
t.Log("------- refresh tokens ------")
|
||||||
|
|
||||||
|
newTokens, err := rp.RefreshAccessToken(provider, refreshToken, "", "")
|
||||||
|
require.NoError(t, err, "refresh token")
|
||||||
|
assert.NotNil(t, newTokens, "access token")
|
||||||
|
t.Logf("new access token %s", newTokens.AccessToken)
|
||||||
|
t.Logf("new refresh token %s", newTokens.RefreshToken)
|
||||||
|
t.Logf("new token type %s", newTokens.TokenType)
|
||||||
|
t.Logf("new expiry %s", newTokens.Expiry.Format(time.RFC3339))
|
||||||
|
require.NotEmpty(t, newTokens.AccessToken, "new accessToken")
|
||||||
|
|
||||||
|
t.Log("------ end session (logout) ------")
|
||||||
|
|
||||||
|
newLoc, err := rp.EndSession(provider, idToken, "", "")
|
||||||
|
require.NoError(t, err, "logout")
|
||||||
|
if newLoc != nil {
|
||||||
|
t.Logf("redirect to %s", newLoc)
|
||||||
|
} else {
|
||||||
|
t.Logf("no redirect")
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Log("------ attempt refresh again (should fail) ------")
|
||||||
|
t.Log("trying original refresh token", refreshToken)
|
||||||
|
_, err = rp.RefreshAccessToken(provider, refreshToken, "", "")
|
||||||
|
assert.Errorf(t, err, "refresh with original")
|
||||||
|
if newTokens.RefreshToken != "" {
|
||||||
|
t.Log("trying replacement refresh token", newTokens.RefreshToken)
|
||||||
|
_, err = rp.RefreshAccessToken(provider, newTokens.RefreshToken, "", "")
|
||||||
|
assert.Errorf(t, err, "refresh with replacement")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResourceServerTokenExchange(t *testing.T) {
|
||||||
|
t.Log("------- start example OP ------")
|
||||||
|
targetURL := "http://local-site"
|
||||||
|
exampleStorage := storage.NewStorage(storage.NewUserStore(targetURL))
|
||||||
|
var dh deferredHandler
|
||||||
|
opServer := httptest.NewServer(&dh)
|
||||||
|
defer opServer.Close()
|
||||||
|
t.Logf("auth server at %s", opServer.URL)
|
||||||
|
dh.Handler = exampleop.SetupServer(opServer.URL, exampleStorage)
|
||||||
|
|
||||||
|
seed := rand.New(rand.NewSource(int64(os.Getpid()) + time.Now().UnixNano()))
|
||||||
|
clientID := t.Name() + "-" + strconv.FormatInt(seed.Int63(), 25)
|
||||||
|
clientSecret := "secret"
|
||||||
|
|
||||||
|
t.Log("------- run authorization code flow ------")
|
||||||
|
provider, _, refreshToken, idToken := RunAuthorizationCodeFlow(t, opServer, clientID, clientSecret)
|
||||||
|
|
||||||
|
resourceServer, err := rs.NewResourceServerClientCredentials(opServer.URL, clientID, clientSecret)
|
||||||
|
require.NoError(t, err, "new resource server")
|
||||||
|
|
||||||
|
t.Log("------- exchage refresh tokens (impersonation) ------")
|
||||||
|
|
||||||
|
tokenExchangeResponse, err := tokenexchange.ExchangeToken(
|
||||||
|
resourceServer,
|
||||||
|
refreshToken,
|
||||||
|
oidc.RefreshTokenType,
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
[]string{},
|
||||||
|
[]string{},
|
||||||
|
[]string{"profile", "custom_scope:impersonate:id2"},
|
||||||
|
oidc.RefreshTokenType,
|
||||||
|
)
|
||||||
|
require.NoError(t, err, "refresh token")
|
||||||
|
require.NotNil(t, tokenExchangeResponse, "token exchange response")
|
||||||
|
assert.Equal(t, tokenExchangeResponse.IssuedTokenType, oidc.RefreshTokenType)
|
||||||
|
assert.NotEmpty(t, tokenExchangeResponse.AccessToken, "access token")
|
||||||
|
assert.NotEmpty(t, tokenExchangeResponse.RefreshToken, "refresh token")
|
||||||
|
assert.Equal(t, []string(tokenExchangeResponse.Scopes), []string{"profile", "custom_scope:impersonate:id2"})
|
||||||
|
|
||||||
|
t.Log("------ end session (logout) ------")
|
||||||
|
|
||||||
|
newLoc, err := rp.EndSession(provider, idToken, "", "")
|
||||||
|
require.NoError(t, err, "logout")
|
||||||
|
if newLoc != nil {
|
||||||
|
t.Logf("redirect to %s", newLoc)
|
||||||
|
} else {
|
||||||
|
t.Logf("no redirect")
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Log("------- attempt exchage again (should fail) ------")
|
||||||
|
|
||||||
|
tokenExchangeResponse, err = tokenexchange.ExchangeToken(
|
||||||
|
resourceServer,
|
||||||
|
refreshToken,
|
||||||
|
oidc.RefreshTokenType,
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
[]string{},
|
||||||
|
[]string{},
|
||||||
|
[]string{"profile", "custom_scope:impersonate:id2"},
|
||||||
|
oidc.RefreshTokenType,
|
||||||
|
)
|
||||||
|
require.Error(t, err, "refresh token")
|
||||||
|
assert.Contains(t, err.Error(), "subject_token is invalid")
|
||||||
|
require.Nil(t, tokenExchangeResponse, "token exchange response")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func RunAuthorizationCodeFlow(t *testing.T, opServer *httptest.Server, clientID, clientSecret string) (provider rp.RelyingParty, accessToken, refreshToken, idToken string) {
|
||||||
targetURL := "http://local-site"
|
targetURL := "http://local-site"
|
||||||
localURL, err := url.Parse(targetURL + "/login?requestID=1234")
|
localURL, err := url.Parse(targetURL + "/login?requestID=1234")
|
||||||
require.NoError(t, err, "local url")
|
require.NoError(t, err, "local url")
|
||||||
|
|
||||||
seed := rand.New(rand.NewSource(int64(os.Getpid()) + time.Now().UnixNano()))
|
client := storage.WebClient(clientID, clientSecret, targetURL)
|
||||||
clientID := t.Name() + "-" + strconv.FormatInt(seed.Int63(), 25)
|
|
||||||
client := storage.WebClient(clientID, "secret", targetURL)
|
|
||||||
storage.RegisterClients(client)
|
storage.RegisterClients(client)
|
||||||
|
|
||||||
jar, err := cookiejar.New(nil)
|
jar, err := cookiejar.New(nil)
|
||||||
|
@ -58,10 +165,10 @@ func TestRelyingPartySession(t *testing.T) {
|
||||||
t.Log("------- create RP ------")
|
t.Log("------- create RP ------")
|
||||||
key := []byte("test1234test1234")
|
key := []byte("test1234test1234")
|
||||||
cookieHandler := httphelper.NewCookieHandler(key, key, httphelper.WithUnsecure())
|
cookieHandler := httphelper.NewCookieHandler(key, key, httphelper.WithUnsecure())
|
||||||
provider, err := rp.NewRelyingPartyOIDC(
|
provider, err = rp.NewRelyingPartyOIDC(
|
||||||
opServer.URL,
|
opServer.URL,
|
||||||
clientID,
|
clientID,
|
||||||
"secret",
|
clientSecret,
|
||||||
targetURL,
|
targetURL,
|
||||||
[]string{"openid", "email", "profile", "offline_access"},
|
[]string{"openid", "email", "profile", "offline_access"},
|
||||||
rp.WithPKCE(cookieHandler),
|
rp.WithPKCE(cookieHandler),
|
||||||
|
@ -70,8 +177,10 @@ func TestRelyingPartySession(t *testing.T) {
|
||||||
rp.WithSupportedSigningAlgorithms("RS256", "RS384", "RS512", "ES256", "ES384", "ES512"),
|
rp.WithSupportedSigningAlgorithms("RS256", "RS384", "RS512", "ES256", "ES384", "ES512"),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
require.NoError(t, err, "new rp")
|
||||||
|
|
||||||
t.Log("------- get redirect from local client (rp) to OP ------")
|
t.Log("------- get redirect from local client (rp) to OP ------")
|
||||||
|
seed := rand.New(rand.NewSource(int64(os.Getpid()) + time.Now().UnixNano()))
|
||||||
state := "state-" + strconv.FormatInt(seed.Int63(), 25)
|
state := "state-" + strconv.FormatInt(seed.Int63(), 25)
|
||||||
capturedW := httptest.NewRecorder()
|
capturedW := httptest.NewRecorder()
|
||||||
get := httptest.NewRequest("GET", localURL.String(), nil)
|
get := httptest.NewRequest("GET", localURL.String(), nil)
|
||||||
|
@ -114,7 +223,7 @@ func TestRelyingPartySession(t *testing.T) {
|
||||||
|
|
||||||
t.Log("------- post to login form, get redirect to OP ------")
|
t.Log("------- post to login form, get redirect to OP ------")
|
||||||
postLoginRedirectURL := fillForm(t, "fill login form", httpClient, form, loginPageURL,
|
postLoginRedirectURL := fillForm(t, "fill login form", httpClient, form, loginPageURL,
|
||||||
gosubmit.Set("username", "test-user"),
|
gosubmit.Set("username", "test-user@local-site"),
|
||||||
gosubmit.Set("password", "verysecure"))
|
gosubmit.Set("password", "verysecure"))
|
||||||
t.Logf("Get redirect from %s", postLoginRedirectURL)
|
t.Logf("Get redirect from %s", postLoginRedirectURL)
|
||||||
|
|
||||||
|
@ -130,19 +239,19 @@ func TestRelyingPartySession(t *testing.T) {
|
||||||
t.Logf("setting cookie %s", cookie)
|
t.Logf("setting cookie %s", cookie)
|
||||||
}
|
}
|
||||||
|
|
||||||
var accessToken, refreshToken, idToken, email string
|
var email string
|
||||||
redirect := func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens, state string, rp rp.RelyingParty, info oidc.UserInfo) {
|
redirect := func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens[*oidc.IDTokenClaims], state string, rp rp.RelyingParty, info *oidc.UserInfo) {
|
||||||
require.NotNil(t, tokens, "tokens")
|
require.NotNil(t, tokens, "tokens")
|
||||||
require.NotNil(t, info, "info")
|
require.NotNil(t, info, "info")
|
||||||
t.Log("access token", tokens.AccessToken)
|
t.Log("access token", tokens.AccessToken)
|
||||||
t.Log("refresh token", tokens.RefreshToken)
|
t.Log("refresh token", tokens.RefreshToken)
|
||||||
t.Log("id token", tokens.IDToken)
|
t.Log("id token", tokens.IDToken)
|
||||||
t.Log("email", info.GetEmail())
|
t.Log("email", info.Email)
|
||||||
|
|
||||||
accessToken = tokens.AccessToken
|
accessToken = tokens.AccessToken
|
||||||
refreshToken = tokens.RefreshToken
|
refreshToken = tokens.RefreshToken
|
||||||
idToken = tokens.IDToken
|
idToken = tokens.IDToken
|
||||||
email = info.GetEmail()
|
email = info.Email
|
||||||
http.Redirect(w, r, targetURL, 302)
|
http.Redirect(w, r, targetURL, 302)
|
||||||
}
|
}
|
||||||
rp.CodeExchangeHandler(rp.UserinfoCallback(redirect), provider, rp.WithURLParam("custom", "param"))(capturedW, get)
|
rp.CodeExchangeHandler(rp.UserinfoCallback(redirect), provider, rp.WithURLParam("custom", "param"))(capturedW, get)
|
||||||
|
@ -154,7 +263,6 @@ func TestRelyingPartySession(t *testing.T) {
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
require.Less(t, capturedW.Code, 400, "token exchange response code")
|
require.Less(t, capturedW.Code, 400, "token exchange response code")
|
||||||
require.Less(t, capturedW.Code, 400, "token exchange response code")
|
|
||||||
// TODO: how to check the custom header was sent to the server?
|
// TODO: how to check the custom header was sent to the server?
|
||||||
|
|
||||||
//nolint:bodyclose
|
//nolint:bodyclose
|
||||||
|
@ -169,43 +277,7 @@ func TestRelyingPartySession(t *testing.T) {
|
||||||
assert.NotEmpty(t, accessToken, "access token")
|
assert.NotEmpty(t, accessToken, "access token")
|
||||||
assert.NotEmpty(t, email, "email")
|
assert.NotEmpty(t, email, "email")
|
||||||
|
|
||||||
t.Log("------- refresh tokens ------")
|
return provider, accessToken, refreshToken, idToken
|
||||||
|
|
||||||
newTokens, err := rp.RefreshAccessToken(provider, refreshToken, "", "")
|
|
||||||
require.NoError(t, err, "refresh token")
|
|
||||||
assert.NotNil(t, newTokens, "access token")
|
|
||||||
t.Logf("new access token %s", newTokens.AccessToken)
|
|
||||||
t.Logf("new refresh token %s", newTokens.RefreshToken)
|
|
||||||
t.Logf("new token type %s", newTokens.TokenType)
|
|
||||||
t.Logf("new expiry %s", newTokens.Expiry.Format(time.RFC3339))
|
|
||||||
require.NotEmpty(t, newTokens.AccessToken, "new accessToken")
|
|
||||||
|
|
||||||
t.Log("------ end session (logout) ------")
|
|
||||||
|
|
||||||
newLoc, err := rp.EndSession(provider, idToken, "", "")
|
|
||||||
require.NoError(t, err, "logout")
|
|
||||||
if newLoc != nil {
|
|
||||||
t.Logf("redirect to %s", newLoc)
|
|
||||||
} else {
|
|
||||||
t.Logf("no redirect")
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Log("------ attempt refresh again (should fail) ------")
|
|
||||||
t.Log("trying original refresh token", refreshToken)
|
|
||||||
_, err = rp.RefreshAccessToken(provider, refreshToken, "", "")
|
|
||||||
assert.Errorf(t, err, "refresh with original")
|
|
||||||
if newTokens.RefreshToken != "" {
|
|
||||||
t.Log("trying replacement refresh token", newTokens.RefreshToken)
|
|
||||||
_, err = rp.RefreshAccessToken(provider, newTokens.RefreshToken, "", "")
|
|
||||||
assert.Errorf(t, err, "refresh with replacement")
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Run("WithPrompt", func(t *testing.T) {
|
|
||||||
opts := rp.WithPrompt("foo", "bar")()
|
|
||||||
url := provider.OAuthConfig().AuthCodeURL("some", opts...)
|
|
||||||
|
|
||||||
require.Contains(t, url, "prompt=foo+bar")
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type deferredHandler struct {
|
type deferredHandler struct {
|
|
@ -5,8 +5,8 @@ import (
|
||||||
|
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
|
|
||||||
"github.com/zitadel/oidc/pkg/http"
|
"github.com/zitadel/oidc/v2/pkg/http"
|
||||||
"github.com/zitadel/oidc/pkg/oidc"
|
"github.com/zitadel/oidc/v2/pkg/oidc"
|
||||||
)
|
)
|
||||||
|
|
||||||
// JWTProfileExchange handles the oauth2 jwt profile exchange
|
// JWTProfileExchange handles the oauth2 jwt profile exchange
|
||||||
|
|
|
@ -7,8 +7,8 @@ import (
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
"gopkg.in/square/go-jose.v2"
|
"gopkg.in/square/go-jose.v2"
|
||||||
|
|
||||||
"github.com/zitadel/oidc/pkg/client"
|
"github.com/zitadel/oidc/v2/pkg/client"
|
||||||
"github.com/zitadel/oidc/pkg/oidc"
|
"github.com/zitadel/oidc/v2/pkg/oidc"
|
||||||
)
|
)
|
||||||
|
|
||||||
// jwtProfileTokenSource implement the oauth2.TokenSource
|
// jwtProfileTokenSource implement the oauth2.TokenSource
|
||||||
|
|
|
@ -4,22 +4,22 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/zitadel/oidc/pkg/client/rp"
|
"github.com/zitadel/oidc/v2/pkg/client/rp"
|
||||||
httphelper "github.com/zitadel/oidc/pkg/http"
|
httphelper "github.com/zitadel/oidc/v2/pkg/http"
|
||||||
"github.com/zitadel/oidc/pkg/oidc"
|
"github.com/zitadel/oidc/v2/pkg/oidc"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
loginPath = "/login"
|
loginPath = "/login"
|
||||||
)
|
)
|
||||||
|
|
||||||
func CodeFlow(ctx context.Context, relyingParty rp.RelyingParty, callbackPath, port string, stateProvider func() string) *oidc.Tokens {
|
func CodeFlow[C oidc.IDClaims](ctx context.Context, relyingParty rp.RelyingParty, callbackPath, port string, stateProvider func() string) *oidc.Tokens[C] {
|
||||||
codeflowCtx, codeflowCancel := context.WithCancel(ctx)
|
codeflowCtx, codeflowCancel := context.WithCancel(ctx)
|
||||||
defer codeflowCancel()
|
defer codeflowCancel()
|
||||||
|
|
||||||
tokenChan := make(chan *oidc.Tokens, 1)
|
tokenChan := make(chan *oidc.Tokens[C], 1)
|
||||||
|
|
||||||
callback := func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens, state string, rp rp.RelyingParty) {
|
callback := func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens[C], state string, rp rp.RelyingParty) {
|
||||||
tokenChan <- tokens
|
tokenChan <- tokens
|
||||||
msg := "<p><strong>Success!</strong></p>"
|
msg := "<p><strong>Success!</strong></p>"
|
||||||
msg = msg + "<p>You are authenticated and can now return to the CLI.</p>"
|
msg = msg + "<p>You are authenticated and can now return to the CLI.</p>"
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
package rp
|
package rp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/zitadel/oidc/pkg/oidc/grants/tokenexchange"
|
"github.com/zitadel/oidc/v2/pkg/oidc/grants/tokenexchange"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DelegationTokenRequest is an implementation of TokenExchangeRequest
|
// DelegationTokenRequest is an implementation of TokenExchangeRequest
|
||||||
// it exchanges an "urn:ietf:params:oauth:token-type:access_token" with an optional
|
// it exchanges an "urn:ietf:params:oauth:token-type:access_token" with an optional
|
||||||
//"urn:ietf:params:oauth:token-type:access_token" actor token for an
|
// "urn:ietf:params:oauth:token-type:access_token" actor token for an
|
||||||
//"urn:ietf:params:oauth:token-type:access_token" delegation token
|
// "urn:ietf:params:oauth:token-type:access_token" delegation token
|
||||||
func DelegationTokenRequest(subjectToken string, opts ...tokenexchange.TokenExchangeOption) *tokenexchange.TokenExchangeRequest {
|
func DelegationTokenRequest(subjectToken string, opts ...tokenexchange.TokenExchangeOption) *tokenexchange.TokenExchangeRequest {
|
||||||
return tokenexchange.NewTokenExchangeRequest(subjectToken, tokenexchange.AccessTokenType, opts...)
|
return tokenexchange.NewTokenExchangeRequest(subjectToken, tokenexchange.AccessTokenType, opts...)
|
||||||
}
|
}
|
||||||
|
|
62
pkg/client/rp/device.go
Normal file
62
pkg/client/rp/device.go
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
package rp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/zitadel/oidc/v2/pkg/client"
|
||||||
|
"github.com/zitadel/oidc/v2/pkg/oidc"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newDeviceClientCredentialsRequest(scopes []string, rp RelyingParty) (*oidc.ClientCredentialsRequest, error) {
|
||||||
|
confg := rp.OAuthConfig()
|
||||||
|
req := &oidc.ClientCredentialsRequest{
|
||||||
|
GrantType: oidc.GrantTypeDeviceCode,
|
||||||
|
Scope: scopes,
|
||||||
|
ClientID: confg.ClientID,
|
||||||
|
ClientSecret: confg.ClientSecret,
|
||||||
|
}
|
||||||
|
|
||||||
|
if signer := rp.Signer(); signer != nil {
|
||||||
|
assertion, err := client.SignedJWTProfileAssertion(rp.OAuthConfig().ClientID, []string{rp.Issuer()}, time.Hour, signer)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to build assertion: %w", err)
|
||||||
|
}
|
||||||
|
req.ClientAssertion = assertion
|
||||||
|
req.ClientAssertionType = oidc.ClientAssertionTypeJWTAssertion
|
||||||
|
}
|
||||||
|
|
||||||
|
return req, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeviceAuthorization starts a new Device Authorization flow as defined
|
||||||
|
// in RFC 8628, section 3.1 and 3.2:
|
||||||
|
// https://www.rfc-editor.org/rfc/rfc8628#section-3.1
|
||||||
|
func DeviceAuthorization(scopes []string, rp RelyingParty) (*oidc.DeviceAuthorizationResponse, error) {
|
||||||
|
req, err := newDeviceClientCredentialsRequest(scopes, rp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return client.CallDeviceAuthorizationEndpoint(req, rp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeviceAccessToken attempts to obtain tokens from a Device Authorization,
|
||||||
|
// by means of polling as defined in RFC, section 3.3 and 3.4:
|
||||||
|
// https://www.rfc-editor.org/rfc/rfc8628#section-3.4
|
||||||
|
func DeviceAccessToken(ctx context.Context, deviceCode string, interval time.Duration, rp RelyingParty) (resp *oidc.AccessTokenResponse, err error) {
|
||||||
|
req := &client.DeviceAccessTokenRequest{
|
||||||
|
DeviceAccessTokenRequest: oidc.DeviceAccessTokenRequest{
|
||||||
|
GrantType: oidc.GrantTypeDeviceCode,
|
||||||
|
DeviceCode: deviceCode,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
req.ClientCredentialsRequest, err = newDeviceClientCredentialsRequest(nil, rp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return client.PollDeviceAccessTokenEndpoint(ctx, interval, req, tokenEndpointCaller{rp})
|
||||||
|
}
|
|
@ -9,8 +9,8 @@ import (
|
||||||
|
|
||||||
"gopkg.in/square/go-jose.v2"
|
"gopkg.in/square/go-jose.v2"
|
||||||
|
|
||||||
httphelper "github.com/zitadel/oidc/pkg/http"
|
httphelper "github.com/zitadel/oidc/v2/pkg/http"
|
||||||
"github.com/zitadel/oidc/pkg/oidc"
|
"github.com/zitadel/oidc/v2/pkg/oidc"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewRemoteKeySet(client *http.Client, jwksURL string, opts ...func(*remoteKeySet)) oidc.KeySet {
|
func NewRemoteKeySet(client *http.Client, jwksURL string, opts ...func(*remoteKeySet)) oidc.KeySet {
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
package mock
|
|
||||||
|
|
||||||
//go:generate mockgen -package mock -destination ./verifier.mock.go github.com/zitadel/oidc/pkg/rp Verifier
|
|
|
@ -1,67 +0,0 @@
|
||||||
// Code generated by MockGen. DO NOT EDIT.
|
|
||||||
// Source: github.com/zitadel/oidc/pkg/rp (interfaces: Verifier)
|
|
||||||
|
|
||||||
// Package mock is a generated GoMock package.
|
|
||||||
package mock
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"reflect"
|
|
||||||
|
|
||||||
"github.com/golang/mock/gomock"
|
|
||||||
|
|
||||||
"github.com/zitadel/oidc/pkg/oidc"
|
|
||||||
)
|
|
||||||
|
|
||||||
// MockVerifier is a mock of Verifier interface
|
|
||||||
type MockVerifier struct {
|
|
||||||
ctrl *gomock.Controller
|
|
||||||
recorder *MockVerifierMockRecorder
|
|
||||||
}
|
|
||||||
|
|
||||||
// MockVerifierMockRecorder is the mock recorder for MockVerifier
|
|
||||||
type MockVerifierMockRecorder struct {
|
|
||||||
mock *MockVerifier
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewMockVerifier creates a new mock instance
|
|
||||||
func NewMockVerifier(ctrl *gomock.Controller) *MockVerifier {
|
|
||||||
mock := &MockVerifier{ctrl: ctrl}
|
|
||||||
mock.recorder = &MockVerifierMockRecorder{mock}
|
|
||||||
return mock
|
|
||||||
}
|
|
||||||
|
|
||||||
// EXPECT returns an object that allows the caller to indicate expected use
|
|
||||||
func (m *MockVerifier) EXPECT() *MockVerifierMockRecorder {
|
|
||||||
return m.recorder
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify mocks base method
|
|
||||||
func (m *MockVerifier) Verify(arg0 context.Context, arg1, arg2 string) (*oidc.IDTokenClaims, error) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "Verify", arg0, arg1, arg2)
|
|
||||||
ret0, _ := ret[0].(*oidc.IDTokenClaims)
|
|
||||||
ret1, _ := ret[1].(error)
|
|
||||||
return ret0, ret1
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify indicates an expected call of Verify
|
|
||||||
func (mr *MockVerifierMockRecorder) Verify(arg0, arg1, arg2 interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Verify", reflect.TypeOf((*MockVerifier)(nil).Verify), arg0, arg1, arg2)
|
|
||||||
}
|
|
||||||
|
|
||||||
// VerifyIDToken mocks base method
|
|
||||||
func (m *MockVerifier) VerifyIDToken(arg0 context.Context, arg1 string) (*oidc.IDTokenClaims, error) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "VerifyIDToken", arg0, arg1)
|
|
||||||
ret0, _ := ret[0].(*oidc.IDTokenClaims)
|
|
||||||
ret1, _ := ret[1].(error)
|
|
||||||
return ret0, ret1
|
|
||||||
}
|
|
||||||
|
|
||||||
// VerifyIDToken indicates an expected call of VerifyIDToken
|
|
||||||
func (mr *MockVerifierMockRecorder) VerifyIDToken(arg0, arg1 interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VerifyIDToken", reflect.TypeOf((*MockVerifier)(nil).VerifyIDToken), arg0, arg1)
|
|
||||||
}
|
|
|
@ -14,9 +14,9 @@ import (
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
"gopkg.in/square/go-jose.v2"
|
"gopkg.in/square/go-jose.v2"
|
||||||
|
|
||||||
"github.com/zitadel/oidc/pkg/client"
|
"github.com/zitadel/oidc/v2/pkg/client"
|
||||||
httphelper "github.com/zitadel/oidc/pkg/http"
|
httphelper "github.com/zitadel/oidc/v2/pkg/http"
|
||||||
"github.com/zitadel/oidc/pkg/oidc"
|
"github.com/zitadel/oidc/v2/pkg/oidc"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -54,11 +54,15 @@ type RelyingParty interface {
|
||||||
GetEndSessionEndpoint() string
|
GetEndSessionEndpoint() string
|
||||||
|
|
||||||
// GetRevokeEndpoint returns the endpoint to revoke a specific token
|
// GetRevokeEndpoint returns the endpoint to revoke a specific token
|
||||||
// "GetRevokeEndpoint() string" will be added in a future release
|
GetRevokeEndpoint() string
|
||||||
|
|
||||||
// UserinfoEndpoint returns the userinfo
|
// UserinfoEndpoint returns the userinfo
|
||||||
UserinfoEndpoint() string
|
UserinfoEndpoint() string
|
||||||
|
|
||||||
|
// GetDeviceAuthorizationEndpoint returns the enpoint which can
|
||||||
|
// be used to start a DeviceAuthorization flow.
|
||||||
|
GetDeviceAuthorizationEndpoint() string
|
||||||
|
|
||||||
// IDTokenVerifier returns the verifier interface used for oidc id_token verification
|
// IDTokenVerifier returns the verifier interface used for oidc id_token verification
|
||||||
IDTokenVerifier() IDTokenVerifier
|
IDTokenVerifier() IDTokenVerifier
|
||||||
// ErrorHandler returns the handler used for callback errors
|
// ErrorHandler returns the handler used for callback errors
|
||||||
|
@ -121,6 +125,10 @@ func (rp *relyingParty) UserinfoEndpoint() string {
|
||||||
return rp.endpoints.UserinfoURL
|
return rp.endpoints.UserinfoURL
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (rp *relyingParty) GetDeviceAuthorizationEndpoint() string {
|
||||||
|
return rp.endpoints.DeviceAuthorizationURL
|
||||||
|
}
|
||||||
|
|
||||||
func (rp *relyingParty) GetEndSessionEndpoint() string {
|
func (rp *relyingParty) GetEndSessionEndpoint() string {
|
||||||
return rp.endpoints.EndSessionURL
|
return rp.endpoints.EndSessionURL
|
||||||
}
|
}
|
||||||
|
@ -371,7 +379,7 @@ func GenerateAndStoreCodeChallenge(w http.ResponseWriter, rp RelyingParty) (stri
|
||||||
|
|
||||||
// CodeExchange handles the oauth2 code exchange, extracting and validating the id_token
|
// CodeExchange handles the oauth2 code exchange, extracting and validating the id_token
|
||||||
// returning it parsed together with the oauth2 tokens (access, refresh)
|
// returning it parsed together with the oauth2 tokens (access, refresh)
|
||||||
func CodeExchange(ctx context.Context, code string, rp RelyingParty, opts ...CodeExchangeOpt) (tokens *oidc.Tokens, err error) {
|
func CodeExchange[C oidc.IDClaims](ctx context.Context, code string, rp RelyingParty, opts ...CodeExchangeOpt) (tokens *oidc.Tokens[C], err error) {
|
||||||
ctx = context.WithValue(ctx, oauth2.HTTPClient, rp.HttpClient())
|
ctx = context.WithValue(ctx, oauth2.HTTPClient, rp.HttpClient())
|
||||||
codeOpts := make([]oauth2.AuthCodeOption, 0)
|
codeOpts := make([]oauth2.AuthCodeOption, 0)
|
||||||
for _, opt := range opts {
|
for _, opt := range opts {
|
||||||
|
@ -384,7 +392,7 @@ func CodeExchange(ctx context.Context, code string, rp RelyingParty, opts ...Cod
|
||||||
}
|
}
|
||||||
|
|
||||||
if rp.IsOAuth2Only() {
|
if rp.IsOAuth2Only() {
|
||||||
return &oidc.Tokens{Token: token}, nil
|
return &oidc.Tokens[C]{Token: token}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
idTokenString, ok := token.Extra(idTokenKey).(string)
|
idTokenString, ok := token.Extra(idTokenKey).(string)
|
||||||
|
@ -392,21 +400,21 @@ func CodeExchange(ctx context.Context, code string, rp RelyingParty, opts ...Cod
|
||||||
return nil, errors.New("id_token missing")
|
return nil, errors.New("id_token missing")
|
||||||
}
|
}
|
||||||
|
|
||||||
idToken, err := VerifyTokens(ctx, token.AccessToken, idTokenString, rp.IDTokenVerifier())
|
idToken, err := VerifyTokens[C](ctx, token.AccessToken, idTokenString, rp.IDTokenVerifier())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &oidc.Tokens{Token: token, IDTokenClaims: idToken, IDToken: idTokenString}, nil
|
return &oidc.Tokens[C]{Token: token, IDTokenClaims: idToken, IDToken: idTokenString}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type CodeExchangeCallback func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens, state string, rp RelyingParty)
|
type CodeExchangeCallback[C oidc.IDClaims] func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens[C], state string, rp RelyingParty)
|
||||||
|
|
||||||
// CodeExchangeHandler extends the `CodeExchange` method with a http handler
|
// CodeExchangeHandler extends the `CodeExchange` method with a http handler
|
||||||
// including cookie handling for secure `state` transfer
|
// including cookie handling for secure `state` transfer
|
||||||
// and optional PKCE code verifier checking.
|
// and optional PKCE code verifier checking.
|
||||||
// Custom paramaters can optionally be set to the token URL.
|
// Custom paramaters can optionally be set to the token URL.
|
||||||
func CodeExchangeHandler(callback CodeExchangeCallback, rp RelyingParty, urlParam ...URLParamOpt) http.HandlerFunc {
|
func CodeExchangeHandler[C oidc.IDClaims](callback CodeExchangeCallback[C], rp RelyingParty, urlParam ...URLParamOpt) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
state, err := tryReadStateCookie(w, r, rp)
|
state, err := tryReadStateCookie(w, r, rp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -439,7 +447,7 @@ func CodeExchangeHandler(callback CodeExchangeCallback, rp RelyingParty, urlPara
|
||||||
}
|
}
|
||||||
codeOpts = append(codeOpts, WithClientAssertionJWT(assertion))
|
codeOpts = append(codeOpts, WithClientAssertionJWT(assertion))
|
||||||
}
|
}
|
||||||
tokens, err := CodeExchange(r.Context(), params.Get("code"), rp, codeOpts...)
|
tokens, err := CodeExchange[C](r.Context(), params.Get("code"), rp, codeOpts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "failed to exchange token: "+err.Error(), http.StatusUnauthorized)
|
http.Error(w, "failed to exchange token: "+err.Error(), http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
|
@ -448,13 +456,13 @@ func CodeExchangeHandler(callback CodeExchangeCallback, rp RelyingParty, urlPara
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type CodeExchangeUserinfoCallback func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens, state string, provider RelyingParty, info oidc.UserInfo)
|
type CodeExchangeUserinfoCallback[C oidc.IDClaims] func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens[C], state string, provider RelyingParty, info *oidc.UserInfo)
|
||||||
|
|
||||||
// UserinfoCallback wraps the callback function of the CodeExchangeHandler
|
// UserinfoCallback wraps the callback function of the CodeExchangeHandler
|
||||||
// and calls the userinfo endpoint with the access token
|
// and calls the userinfo endpoint with the access token
|
||||||
// on success it will pass the userinfo into its callback function as well
|
// on success it will pass the userinfo into its callback function as well
|
||||||
func UserinfoCallback(f CodeExchangeUserinfoCallback) CodeExchangeCallback {
|
func UserinfoCallback[C oidc.IDClaims](f CodeExchangeUserinfoCallback[C]) CodeExchangeCallback[C] {
|
||||||
return func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens, state string, rp RelyingParty) {
|
return func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens[C], state string, rp RelyingParty) {
|
||||||
info, err := Userinfo(tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.GetSubject(), rp)
|
info, err := Userinfo(tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.GetSubject(), rp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "userinfo failed: "+err.Error(), http.StatusUnauthorized)
|
http.Error(w, "userinfo failed: "+err.Error(), http.StatusUnauthorized)
|
||||||
|
@ -465,17 +473,17 @@ func UserinfoCallback(f CodeExchangeUserinfoCallback) CodeExchangeCallback {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Userinfo will call the OIDC Userinfo Endpoint with the provided token
|
// Userinfo will call the OIDC Userinfo Endpoint with the provided token
|
||||||
func Userinfo(token, tokenType, subject string, rp RelyingParty) (oidc.UserInfo, error) {
|
func Userinfo(token, tokenType, subject string, rp RelyingParty) (*oidc.UserInfo, error) {
|
||||||
req, err := http.NewRequest("GET", rp.UserinfoEndpoint(), nil)
|
req, err := http.NewRequest("GET", rp.UserinfoEndpoint(), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
req.Header.Set("authorization", tokenType+" "+token)
|
req.Header.Set("authorization", tokenType+" "+token)
|
||||||
userinfo := oidc.NewUserInfo()
|
userinfo := new(oidc.UserInfo)
|
||||||
if err := httphelper.HttpRequest(rp.HttpClient(), req, &userinfo); err != nil {
|
if err := httphelper.HttpRequest(rp.HttpClient(), req, &userinfo); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if userinfo.GetSubject() != subject {
|
if userinfo.Subject != subject {
|
||||||
return nil, ErrUserInfoSubNotMatching
|
return nil, ErrUserInfoSubNotMatching
|
||||||
}
|
}
|
||||||
return userinfo, nil
|
return userinfo, nil
|
||||||
|
@ -511,6 +519,7 @@ type Endpoints struct {
|
||||||
JKWsURL string
|
JKWsURL string
|
||||||
EndSessionURL string
|
EndSessionURL string
|
||||||
RevokeURL string
|
RevokeURL string
|
||||||
|
DeviceAuthorizationURL string
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetEndpoints(discoveryConfig *oidc.DiscoveryConfiguration) Endpoints {
|
func GetEndpoints(discoveryConfig *oidc.DiscoveryConfiguration) Endpoints {
|
||||||
|
@ -525,6 +534,7 @@ func GetEndpoints(discoveryConfig *oidc.DiscoveryConfiguration) Endpoints {
|
||||||
JKWsURL: discoveryConfig.JwksURI,
|
JKWsURL: discoveryConfig.JwksURI,
|
||||||
EndSessionURL: discoveryConfig.EndSessionEndpoint,
|
EndSessionURL: discoveryConfig.EndSessionEndpoint,
|
||||||
RevokeURL: discoveryConfig.RevocationEndpoint,
|
RevokeURL: discoveryConfig.RevocationEndpoint,
|
||||||
|
DeviceAuthorizationURL: discoveryConfig.DeviceAuthorizationEndpoint,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ import (
|
||||||
|
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
|
|
||||||
"github.com/zitadel/oidc/pkg/oidc/grants/tokenexchange"
|
"github.com/zitadel/oidc/v2/pkg/oidc/grants/tokenexchange"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TokenExchangeRP extends the `RelyingParty` interface for the *draft* oauth2 `Token Exchange`
|
// TokenExchangeRP extends the `RelyingParty` interface for the *draft* oauth2 `Token Exchange`
|
||||||
|
|
|
@ -6,7 +6,7 @@ import (
|
||||||
|
|
||||||
"gopkg.in/square/go-jose.v2"
|
"gopkg.in/square/go-jose.v2"
|
||||||
|
|
||||||
"github.com/zitadel/oidc/pkg/oidc"
|
"github.com/zitadel/oidc/v2/pkg/oidc"
|
||||||
)
|
)
|
||||||
|
|
||||||
type IDTokenVerifier interface {
|
type IDTokenVerifier interface {
|
||||||
|
@ -20,76 +20,78 @@ type IDTokenVerifier interface {
|
||||||
}
|
}
|
||||||
|
|
||||||
// VerifyTokens implement the Token Response Validation as defined in OIDC specification
|
// VerifyTokens implement the Token Response Validation as defined in OIDC specification
|
||||||
//https://openid.net/specs/openid-connect-core-1_0.html#TokenResponseValidation
|
// https://openid.net/specs/openid-connect-core-1_0.html#TokenResponseValidation
|
||||||
func VerifyTokens(ctx context.Context, accessToken, idTokenString string, v IDTokenVerifier) (oidc.IDTokenClaims, error) {
|
func VerifyTokens[C oidc.IDClaims](ctx context.Context, accessToken, idToken string, v IDTokenVerifier) (claims C, err error) {
|
||||||
idToken, err := VerifyIDToken(ctx, idTokenString, v)
|
var nilClaims C
|
||||||
|
|
||||||
|
claims, err = VerifyIDToken[C](ctx, idToken, v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nilClaims, err
|
||||||
}
|
}
|
||||||
if err := VerifyAccessToken(accessToken, idToken.GetAccessTokenHash(), idToken.GetSignatureAlgorithm()); err != nil {
|
if err := VerifyAccessToken(accessToken, claims.GetAccessTokenHash(), claims.GetSignatureAlgorithm()); err != nil {
|
||||||
return nil, err
|
return nilClaims, err
|
||||||
}
|
}
|
||||||
return idToken, nil
|
return claims, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// VerifyIDToken validates the id token according to
|
// VerifyIDToken validates the id token according to
|
||||||
//https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
|
// https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
|
||||||
func VerifyIDToken(ctx context.Context, token string, v IDTokenVerifier) (oidc.IDTokenClaims, error) {
|
func VerifyIDToken[C oidc.Claims](ctx context.Context, token string, v IDTokenVerifier) (claims C, err error) {
|
||||||
claims := oidc.EmptyIDTokenClaims()
|
var nilClaims C
|
||||||
|
|
||||||
decrypted, err := oidc.DecryptToken(token)
|
decrypted, err := oidc.DecryptToken(token)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nilClaims, err
|
||||||
}
|
}
|
||||||
payload, err := oidc.ParseToken(decrypted, claims)
|
payload, err := oidc.ParseToken(decrypted, &claims)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nilClaims, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := oidc.CheckSubject(claims); err != nil {
|
if err := oidc.CheckSubject(claims); err != nil {
|
||||||
return nil, err
|
return nilClaims, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = oidc.CheckIssuer(claims, v.Issuer()); err != nil {
|
if err = oidc.CheckIssuer(claims, v.Issuer()); err != nil {
|
||||||
return nil, err
|
return nilClaims, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = oidc.CheckAudience(claims, v.ClientID()); err != nil {
|
if err = oidc.CheckAudience(claims, v.ClientID()); err != nil {
|
||||||
return nil, err
|
return nilClaims, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = oidc.CheckAuthorizedParty(claims, v.ClientID()); err != nil {
|
if err = oidc.CheckAuthorizedParty(claims, v.ClientID()); err != nil {
|
||||||
return nil, err
|
return nilClaims, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = oidc.CheckSignature(ctx, decrypted, payload, claims, v.SupportedSignAlgs(), v.KeySet()); err != nil {
|
if err = oidc.CheckSignature(ctx, decrypted, payload, claims, v.SupportedSignAlgs(), v.KeySet()); err != nil {
|
||||||
return nil, err
|
return nilClaims, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = oidc.CheckExpiration(claims, v.Offset()); err != nil {
|
if err = oidc.CheckExpiration(claims, v.Offset()); err != nil {
|
||||||
return nil, err
|
return nilClaims, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = oidc.CheckIssuedAt(claims, v.MaxAgeIAT(), v.Offset()); err != nil {
|
if err = oidc.CheckIssuedAt(claims, v.MaxAgeIAT(), v.Offset()); err != nil {
|
||||||
return nil, err
|
return nilClaims, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = oidc.CheckNonce(claims, v.Nonce(ctx)); err != nil {
|
if err = oidc.CheckNonce(claims, v.Nonce(ctx)); err != nil {
|
||||||
return nil, err
|
return nilClaims, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = oidc.CheckAuthorizationContextClassReference(claims, v.ACR()); err != nil {
|
if err = oidc.CheckAuthorizationContextClassReference(claims, v.ACR()); err != nil {
|
||||||
return nil, err
|
return nilClaims, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = oidc.CheckAuthTime(claims, v.MaxAge()); err != nil {
|
if err = oidc.CheckAuthTime(claims, v.MaxAge()); err != nil {
|
||||||
return nil, err
|
return nilClaims, err
|
||||||
}
|
}
|
||||||
return claims, nil
|
return claims, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// VerifyAccessToken validates the access token according to
|
// VerifyAccessToken validates the access token according to
|
||||||
//https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowTokenValidation
|
// https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowTokenValidation
|
||||||
func VerifyAccessToken(accessToken, atHash string, sigAlgorithm jose.SignatureAlgorithm) error {
|
func VerifyAccessToken(accessToken, atHash string, sigAlgorithm jose.SignatureAlgorithm) error {
|
||||||
if atHash == "" {
|
if atHash == "" {
|
||||||
return nil
|
return nil
|
||||||
|
@ -112,7 +114,7 @@ func NewIDTokenVerifier(issuer, clientID string, keySet oidc.KeySet, options ...
|
||||||
issuer: issuer,
|
issuer: issuer,
|
||||||
clientID: clientID,
|
clientID: clientID,
|
||||||
keySet: keySet,
|
keySet: keySet,
|
||||||
offset: 1 * time.Second,
|
offset: time.Second,
|
||||||
nonce: func(_ context.Context) string {
|
nonce: func(_ context.Context) string {
|
||||||
return ""
|
return ""
|
||||||
},
|
},
|
||||||
|
@ -139,7 +141,7 @@ func WithIssuedAtOffset(offset time.Duration) func(*idTokenVerifier) {
|
||||||
// WithIssuedAtMaxAge provides the ability to define the maximum duration between iat and now
|
// WithIssuedAtMaxAge provides the ability to define the maximum duration between iat and now
|
||||||
func WithIssuedAtMaxAge(maxAge time.Duration) func(*idTokenVerifier) {
|
func WithIssuedAtMaxAge(maxAge time.Duration) func(*idTokenVerifier) {
|
||||||
return func(v *idTokenVerifier) {
|
return func(v *idTokenVerifier) {
|
||||||
v.maxAge = maxAge
|
v.maxAgeIAT = maxAge
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
339
pkg/client/rp/verifier_test.go
Normal file
339
pkg/client/rp/verifier_test.go
Normal file
|
@ -0,0 +1,339 @@
|
||||||
|
package rp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
tu "github.com/zitadel/oidc/v2/internal/testutil"
|
||||||
|
"github.com/zitadel/oidc/v2/pkg/oidc"
|
||||||
|
"gopkg.in/square/go-jose.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestVerifyTokens(t *testing.T) {
|
||||||
|
verifier := &idTokenVerifier{
|
||||||
|
issuer: tu.ValidIssuer,
|
||||||
|
maxAgeIAT: 2 * time.Minute,
|
||||||
|
offset: time.Second,
|
||||||
|
supportedSignAlgs: []string{string(tu.SignatureAlgorithm)},
|
||||||
|
keySet: tu.KeySet{},
|
||||||
|
maxAge: 2 * time.Minute,
|
||||||
|
acr: tu.ACRVerify,
|
||||||
|
nonce: func(context.Context) string { return tu.ValidNonce },
|
||||||
|
clientID: tu.ValidClientID,
|
||||||
|
}
|
||||||
|
accessToken, _ := tu.ValidAccessToken()
|
||||||
|
atHash, err := oidc.ClaimHash(accessToken, tu.SignatureAlgorithm)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
accessToken string
|
||||||
|
idTokenClaims func() (string, *oidc.IDTokenClaims)
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "without access token",
|
||||||
|
idTokenClaims: tu.ValidIDToken,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with access token",
|
||||||
|
accessToken: accessToken,
|
||||||
|
idTokenClaims: func() (string, *oidc.IDTokenClaims) {
|
||||||
|
return tu.NewIDToken(
|
||||||
|
tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience,
|
||||||
|
tu.ValidExpiration, tu.ValidAuthTime, tu.ValidNonce,
|
||||||
|
tu.ValidACR, tu.ValidAMR, tu.ValidClientID, tu.ValidSkew, atHash,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "expired id token",
|
||||||
|
accessToken: accessToken,
|
||||||
|
idTokenClaims: func() (string, *oidc.IDTokenClaims) {
|
||||||
|
return tu.NewIDToken(
|
||||||
|
tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience,
|
||||||
|
tu.ValidExpiration.Add(-time.Hour), tu.ValidAuthTime, tu.ValidNonce,
|
||||||
|
tu.ValidACR, tu.ValidAMR, tu.ValidClientID, tu.ValidSkew, atHash,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wrong access token",
|
||||||
|
accessToken: accessToken,
|
||||||
|
idTokenClaims: func() (string, *oidc.IDTokenClaims) {
|
||||||
|
return tu.NewIDToken(
|
||||||
|
tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience,
|
||||||
|
tu.ValidExpiration, tu.ValidAuthTime, tu.ValidNonce,
|
||||||
|
tu.ValidACR, tu.ValidAMR, tu.ValidClientID, tu.ValidSkew, "~~~",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
idToken, want := tt.idTokenClaims()
|
||||||
|
got, err := VerifyTokens[*oidc.IDTokenClaims](context.Background(), tt.accessToken, idToken, verifier)
|
||||||
|
if tt.wantErr {
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Nil(t, got)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, got)
|
||||||
|
assert.Equal(t, got, want)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVerifyIDToken(t *testing.T) {
|
||||||
|
verifier := &idTokenVerifier{
|
||||||
|
issuer: tu.ValidIssuer,
|
||||||
|
maxAgeIAT: 2 * time.Minute,
|
||||||
|
offset: time.Second,
|
||||||
|
supportedSignAlgs: []string{string(tu.SignatureAlgorithm)},
|
||||||
|
keySet: tu.KeySet{},
|
||||||
|
maxAge: 2 * time.Minute,
|
||||||
|
acr: tu.ACRVerify,
|
||||||
|
nonce: func(context.Context) string { return tu.ValidNonce },
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
clientID string
|
||||||
|
tokenClaims func() (string, *oidc.IDTokenClaims)
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "success",
|
||||||
|
clientID: tu.ValidClientID,
|
||||||
|
tokenClaims: tu.ValidIDToken,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "parse err",
|
||||||
|
clientID: tu.ValidClientID,
|
||||||
|
tokenClaims: func() (string, *oidc.IDTokenClaims) { return "~~~~", nil },
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid signature",
|
||||||
|
clientID: tu.ValidClientID,
|
||||||
|
tokenClaims: func() (string, *oidc.IDTokenClaims) { return tu.InvalidSignatureToken, nil },
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty subject",
|
||||||
|
clientID: tu.ValidClientID,
|
||||||
|
tokenClaims: func() (string, *oidc.IDTokenClaims) {
|
||||||
|
return tu.NewIDToken(
|
||||||
|
tu.ValidIssuer, "", tu.ValidAudience,
|
||||||
|
tu.ValidExpiration, tu.ValidAuthTime, tu.ValidNonce,
|
||||||
|
tu.ValidACR, tu.ValidAMR, tu.ValidClientID, tu.ValidSkew, "",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wrong issuer",
|
||||||
|
clientID: tu.ValidClientID,
|
||||||
|
tokenClaims: func() (string, *oidc.IDTokenClaims) {
|
||||||
|
return tu.NewIDToken(
|
||||||
|
"foo", tu.ValidSubject, tu.ValidAudience,
|
||||||
|
tu.ValidExpiration, tu.ValidAuthTime, tu.ValidNonce,
|
||||||
|
tu.ValidACR, tu.ValidAMR, tu.ValidClientID, tu.ValidSkew, "",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wrong clientID",
|
||||||
|
clientID: "foo",
|
||||||
|
tokenClaims: tu.ValidIDToken,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "expired",
|
||||||
|
clientID: tu.ValidClientID,
|
||||||
|
tokenClaims: func() (string, *oidc.IDTokenClaims) {
|
||||||
|
return tu.NewIDToken(
|
||||||
|
tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience,
|
||||||
|
tu.ValidExpiration.Add(-time.Hour), tu.ValidAuthTime, tu.ValidNonce,
|
||||||
|
tu.ValidACR, tu.ValidAMR, tu.ValidClientID, tu.ValidSkew, "",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wrong IAT",
|
||||||
|
clientID: tu.ValidClientID,
|
||||||
|
tokenClaims: func() (string, *oidc.IDTokenClaims) {
|
||||||
|
return tu.NewIDToken(
|
||||||
|
tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience,
|
||||||
|
tu.ValidExpiration, tu.ValidAuthTime, tu.ValidNonce,
|
||||||
|
tu.ValidACR, tu.ValidAMR, tu.ValidClientID, -time.Hour, "",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wrong acr",
|
||||||
|
clientID: tu.ValidClientID,
|
||||||
|
tokenClaims: func() (string, *oidc.IDTokenClaims) {
|
||||||
|
return tu.NewIDToken(
|
||||||
|
tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience,
|
||||||
|
tu.ValidExpiration, tu.ValidAuthTime, tu.ValidNonce,
|
||||||
|
"else", tu.ValidAMR, tu.ValidClientID, tu.ValidSkew, "",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "expired auth",
|
||||||
|
clientID: tu.ValidClientID,
|
||||||
|
tokenClaims: func() (string, *oidc.IDTokenClaims) {
|
||||||
|
return tu.NewIDToken(
|
||||||
|
tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience,
|
||||||
|
tu.ValidExpiration, tu.ValidAuthTime.Add(-time.Hour), tu.ValidNonce,
|
||||||
|
tu.ValidACR, tu.ValidAMR, tu.ValidClientID, tu.ValidSkew, "",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wrong nonce",
|
||||||
|
clientID: tu.ValidClientID,
|
||||||
|
tokenClaims: func() (string, *oidc.IDTokenClaims) {
|
||||||
|
return tu.NewIDToken(
|
||||||
|
tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience,
|
||||||
|
tu.ValidExpiration, tu.ValidAuthTime, "foo",
|
||||||
|
tu.ValidACR, tu.ValidAMR, tu.ValidClientID, tu.ValidSkew, "",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
token, want := tt.tokenClaims()
|
||||||
|
verifier.clientID = tt.clientID
|
||||||
|
got, err := VerifyIDToken[*oidc.IDTokenClaims](context.Background(), token, verifier)
|
||||||
|
if tt.wantErr {
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Nil(t, got)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, got)
|
||||||
|
assert.Equal(t, got, want)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVerifyAccessToken(t *testing.T) {
|
||||||
|
token, _ := tu.ValidAccessToken()
|
||||||
|
hash, err := oidc.ClaimHash(token, tu.SignatureAlgorithm)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
type args struct {
|
||||||
|
accessToken string
|
||||||
|
atHash string
|
||||||
|
sigAlgorithm jose.SignatureAlgorithm
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "empty hash",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "success",
|
||||||
|
args: args{
|
||||||
|
accessToken: token,
|
||||||
|
atHash: hash,
|
||||||
|
sigAlgorithm: tu.SignatureAlgorithm,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid algorithm",
|
||||||
|
args: args{
|
||||||
|
accessToken: token,
|
||||||
|
atHash: hash,
|
||||||
|
sigAlgorithm: "foo",
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "mismatch",
|
||||||
|
args: args{
|
||||||
|
accessToken: token,
|
||||||
|
atHash: "~~",
|
||||||
|
sigAlgorithm: tu.SignatureAlgorithm,
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
err := VerifyAccessToken(tt.args.accessToken, tt.args.atHash, tt.args.sigAlgorithm)
|
||||||
|
if tt.wantErr {
|
||||||
|
assert.Error(t, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
require.NoError(t, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewIDTokenVerifier(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
issuer string
|
||||||
|
clientID string
|
||||||
|
keySet oidc.KeySet
|
||||||
|
options []VerifierOption
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
want IDTokenVerifier
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "nil nonce", // otherwise assert.Equal will fail on the function
|
||||||
|
args: args{
|
||||||
|
issuer: tu.ValidIssuer,
|
||||||
|
clientID: tu.ValidClientID,
|
||||||
|
keySet: tu.KeySet{},
|
||||||
|
options: []VerifierOption{
|
||||||
|
WithIssuedAtOffset(time.Minute),
|
||||||
|
WithIssuedAtMaxAge(time.Hour),
|
||||||
|
WithNonce(nil), // otherwise assert.Equal will fail on the function
|
||||||
|
WithACRVerifier(nil),
|
||||||
|
WithAuthTimeMaxAge(2 * time.Hour),
|
||||||
|
WithSupportedSigningAlgorithms("ABC", "DEF"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: &idTokenVerifier{
|
||||||
|
issuer: tu.ValidIssuer,
|
||||||
|
offset: time.Minute,
|
||||||
|
maxAgeIAT: time.Hour,
|
||||||
|
clientID: tu.ValidClientID,
|
||||||
|
keySet: tu.KeySet{},
|
||||||
|
nonce: nil,
|
||||||
|
acr: nil,
|
||||||
|
maxAge: 2 * time.Hour,
|
||||||
|
supportedSignAlgs: []string{"ABC", "DEF"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got := NewIDTokenVerifier(tt.args.issuer, tt.args.clientID, tt.args.keySet, tt.args.options...)
|
||||||
|
assert.Equal(t, tt.want, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
86
pkg/client/rp/verifier_tokens_example_test.go
Normal file
86
pkg/client/rp/verifier_tokens_example_test.go
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
package rp_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
tu "github.com/zitadel/oidc/v2/internal/testutil"
|
||||||
|
"github.com/zitadel/oidc/v2/pkg/client/rp"
|
||||||
|
"github.com/zitadel/oidc/v2/pkg/oidc"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MyCustomClaims extends the TokenClaims base,
|
||||||
|
// so it implmeents the oidc.Claims interface.
|
||||||
|
// Instead of carrying a map, we add needed fields// to the struct for type safe access.
|
||||||
|
type MyCustomClaims struct {
|
||||||
|
oidc.TokenClaims
|
||||||
|
NotBefore oidc.Time `json:"nbf,omitempty"`
|
||||||
|
AccessTokenHash string `json:"at_hash,omitempty"`
|
||||||
|
Foo string `json:"foo,omitempty"`
|
||||||
|
Bar *Nested `json:"bar,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAccessTokenHash is required to implement
|
||||||
|
// the oidc.IDClaims interface.
|
||||||
|
func (c *MyCustomClaims) GetAccessTokenHash() string {
|
||||||
|
return c.AccessTokenHash
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nested struct types are also possible.
|
||||||
|
type Nested struct {
|
||||||
|
Count int `json:"count,omitempty"`
|
||||||
|
Tags []string `json:"tags,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
idToken carries the following claims. foo and bar are custom claims
|
||||||
|
|
||||||
|
{
|
||||||
|
"acr": "something",
|
||||||
|
"amr": [
|
||||||
|
"foo",
|
||||||
|
"bar"
|
||||||
|
],
|
||||||
|
"at_hash": "2dzbm_vIxy-7eRtqUIGPPw",
|
||||||
|
"aud": [
|
||||||
|
"unit",
|
||||||
|
"test",
|
||||||
|
"555666"
|
||||||
|
],
|
||||||
|
"auth_time": 1678100961,
|
||||||
|
"azp": "555666",
|
||||||
|
"bar": {
|
||||||
|
"count": 22,
|
||||||
|
"tags": [
|
||||||
|
"some",
|
||||||
|
"tags"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"client_id": "555666",
|
||||||
|
"exp": 4802238682,
|
||||||
|
"foo": "Hello, World!",
|
||||||
|
"iat": 1678101021,
|
||||||
|
"iss": "local.com",
|
||||||
|
"jti": "9876",
|
||||||
|
"nbf": 1678101021,
|
||||||
|
"nonce": "12345",
|
||||||
|
"sub": "tim@local.com"
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
const idToken = `eyJhbGciOiJSUzI1NiIsImtpZCI6IjEifQ.eyJhY3IiOiJzb21ldGhpbmciLCJhbXIiOlsiZm9vIiwiYmFyIl0sImF0X2hhc2giOiIyZHpibV92SXh5LTdlUnRxVUlHUFB3IiwiYXVkIjpbInVuaXQiLCJ0ZXN0IiwiNTU1NjY2Il0sImF1dGhfdGltZSI6MTY3ODEwMDk2MSwiYXpwIjoiNTU1NjY2IiwiYmFyIjp7ImNvdW50IjoyMiwidGFncyI6WyJzb21lIiwidGFncyJdfSwiY2xpZW50X2lkIjoiNTU1NjY2IiwiZXhwIjo0ODAyMjM4NjgyLCJmb28iOiJIZWxsbywgV29ybGQhIiwiaWF0IjoxNjc4MTAxMDIxLCJpc3MiOiJsb2NhbC5jb20iLCJqdGkiOiI5ODc2IiwibmJmIjoxNjc4MTAxMDIxLCJub25jZSI6IjEyMzQ1Iiwic3ViIjoidGltQGxvY2FsLmNvbSJ9.t3GXSfVNNwiW1Suv9_84v0sdn2_-RWHVxhphhRozDXnsO7SDNOlGnEioemXABESxSzMclM7gB7mYy5Qah2ZUNx7eP5t2njoxEYfavgHwx7UJZ2NCg8NDPQyr-hlxelEcfdXK-I0oTd-FRDvF4rqPkD9Us52IpnplChCxnHFgh4wKwPqZZjv2IXVCtn0ilKW3hff1rMOYKEuLRcN2YP0gkyuqyHvcf2dMmjod0t4sLOTJ82rsCbMBC5CLpqv3nIC9HOGITkt1Kd-Am0n1LrdZvWwTo6RFe8AnzF0gpqjcB5Wg4Qeh58DIjZOz4f_8wnmJ_gCqyRh5vfSW4XHdbum0Tw`
|
||||||
|
const accessToken = `eyJhbGciOiJSUzI1NiIsImtpZCI6IjEifQ.eyJhdWQiOlsidW5pdCIsInRlc3QiXSwiYmFyIjp7ImNvdW50IjoyMiwidGFncyI6WyJzb21lIiwidGFncyJdfSwiZXhwIjo0ODAyMjM4NjgyLCJmb28iOiJIZWxsbywgV29ybGQhIiwiaWF0IjoxNjc4MTAxMDIxLCJpc3MiOiJsb2NhbC5jb20iLCJqdGkiOiI5ODc2IiwibmJmIjoxNjc4MTAxMDIxLCJzdWIiOiJ0aW1AbG9jYWwuY29tIn0.Zrz3LWSRjCMJZUMaI5dUbW4vGdSmEeJQ3ouhaX0bcW9rdFFLgBI4K2FWJhNivq8JDmCGSxwLu3mI680GWmDaEoAx1M5sCO9lqfIZHGZh-lfAXk27e6FPLlkTDBq8Bx4o4DJ9Fw0hRJGjUTjnYv5cq1vo2-UqldasL6CwTbkzNC_4oQFfRtuodC4Ql7dZ1HRv5LXuYx7KPkOssLZtV9cwtJp5nFzKjcf2zEE_tlbjcpynMwypornRUp1EhCWKRUGkJhJeiP71ECY5pQhShfjBu9Nc5wDpSnZmnk2S4YsPrRK3QkE-iEkas8BfsOCrGoErHjEJexAIDjasGO5PFLWfCA`
|
||||||
|
|
||||||
|
func ExampleVerifyTokens_customClaims() {
|
||||||
|
v := rp.NewIDTokenVerifier("local.com", "555666", tu.KeySet{},
|
||||||
|
rp.WithNonce(func(ctx context.Context) string { return "12345" }),
|
||||||
|
)
|
||||||
|
|
||||||
|
// VerifyAccessToken can be called with the *MyCustomClaims.
|
||||||
|
claims, err := rp.VerifyTokens[*MyCustomClaims](context.TODO(), accessToken, idToken, v)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
// Here we have typesafe access to the custom claims
|
||||||
|
fmt.Println(claims.Foo, claims.Bar.Count, claims.Bar.Tags)
|
||||||
|
// Output: Hello, World! 22 [some tags]
|
||||||
|
}
|
|
@ -6,13 +6,14 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/zitadel/oidc/pkg/client"
|
"github.com/zitadel/oidc/v2/pkg/client"
|
||||||
httphelper "github.com/zitadel/oidc/pkg/http"
|
httphelper "github.com/zitadel/oidc/v2/pkg/http"
|
||||||
"github.com/zitadel/oidc/pkg/oidc"
|
"github.com/zitadel/oidc/v2/pkg/oidc"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ResourceServer interface {
|
type ResourceServer interface {
|
||||||
IntrospectionURL() string
|
IntrospectionURL() string
|
||||||
|
TokenEndpoint() string
|
||||||
HttpClient() *http.Client
|
HttpClient() *http.Client
|
||||||
AuthFn() (interface{}, error)
|
AuthFn() (interface{}, error)
|
||||||
}
|
}
|
||||||
|
@ -29,6 +30,10 @@ func (r *resourceServer) IntrospectionURL() string {
|
||||||
return r.introspectURL
|
return r.introspectURL
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *resourceServer) TokenEndpoint() string {
|
||||||
|
return r.tokenURL
|
||||||
|
}
|
||||||
|
|
||||||
func (r *resourceServer) HttpClient() *http.Client {
|
func (r *resourceServer) HttpClient() *http.Client {
|
||||||
return r.httpClient
|
return r.httpClient
|
||||||
}
|
}
|
||||||
|
@ -107,7 +112,7 @@ func WithStaticEndpoints(tokenURL, introspectURL string) Option {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Introspect(ctx context.Context, rp ResourceServer, token string) (oidc.IntrospectionResponse, error) {
|
func Introspect(ctx context.Context, rp ResourceServer, token string) (*oidc.IntrospectionResponse, error) {
|
||||||
authFn, err := rp.AuthFn()
|
authFn, err := rp.AuthFn()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -116,7 +121,7 @@ func Introspect(ctx context.Context, rp ResourceServer, token string) (oidc.Intr
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
resp := oidc.NewIntrospectionResponse()
|
resp := new(oidc.IntrospectionResponse)
|
||||||
if err := httphelper.HttpRequest(rp.HttpClient(), req, resp); err != nil {
|
if err := httphelper.HttpRequest(rp.HttpClient(), req, resp); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
127
pkg/client/tokenexchange/tokenexchange.go
Normal file
127
pkg/client/tokenexchange/tokenexchange.go
Normal file
|
@ -0,0 +1,127 @@
|
||||||
|
package tokenexchange
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/zitadel/oidc/v2/pkg/client"
|
||||||
|
httphelper "github.com/zitadel/oidc/v2/pkg/http"
|
||||||
|
"github.com/zitadel/oidc/v2/pkg/oidc"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TokenExchanger interface {
|
||||||
|
TokenEndpoint() string
|
||||||
|
HttpClient() *http.Client
|
||||||
|
AuthFn() (interface{}, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type OAuthTokenExchange struct {
|
||||||
|
httpClient *http.Client
|
||||||
|
tokenEndpoint string
|
||||||
|
authFn func() (interface{}, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTokenExchanger(issuer string, options ...func(source *OAuthTokenExchange)) (TokenExchanger, error) {
|
||||||
|
return newOAuthTokenExchange(issuer, nil, options...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTokenExchangerClientCredentials(issuer, clientID, clientSecret string, options ...func(source *OAuthTokenExchange)) (TokenExchanger, error) {
|
||||||
|
authorizer := func() (interface{}, error) {
|
||||||
|
return httphelper.AuthorizeBasic(clientID, clientSecret), nil
|
||||||
|
}
|
||||||
|
return newOAuthTokenExchange(issuer, authorizer, options...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newOAuthTokenExchange(issuer string, authorizer func() (interface{}, error), options ...func(source *OAuthTokenExchange)) (*OAuthTokenExchange, error) {
|
||||||
|
te := &OAuthTokenExchange{
|
||||||
|
httpClient: httphelper.DefaultHTTPClient,
|
||||||
|
}
|
||||||
|
for _, opt := range options {
|
||||||
|
opt(te)
|
||||||
|
}
|
||||||
|
|
||||||
|
if te.tokenEndpoint == "" {
|
||||||
|
config, err := client.Discover(issuer, te.httpClient)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
te.tokenEndpoint = config.TokenEndpoint
|
||||||
|
}
|
||||||
|
|
||||||
|
if te.tokenEndpoint == "" {
|
||||||
|
return nil, errors.New("tokenURL is empty: please provide with either `WithStaticTokenEndpoint` or a discovery url")
|
||||||
|
}
|
||||||
|
|
||||||
|
te.authFn = authorizer
|
||||||
|
|
||||||
|
return te, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithHTTPClient(client *http.Client) func(*OAuthTokenExchange) {
|
||||||
|
return func(source *OAuthTokenExchange) {
|
||||||
|
source.httpClient = client
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithStaticTokenEndpoint(issuer, tokenEndpoint string) func(*OAuthTokenExchange) {
|
||||||
|
return func(source *OAuthTokenExchange) {
|
||||||
|
source.tokenEndpoint = tokenEndpoint
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (te *OAuthTokenExchange) TokenEndpoint() string {
|
||||||
|
return te.tokenEndpoint
|
||||||
|
}
|
||||||
|
|
||||||
|
func (te *OAuthTokenExchange) HttpClient() *http.Client {
|
||||||
|
return te.httpClient
|
||||||
|
}
|
||||||
|
|
||||||
|
func (te *OAuthTokenExchange) AuthFn() (interface{}, error) {
|
||||||
|
if te.authFn != nil {
|
||||||
|
return te.authFn()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExchangeToken sends a token exchange request (rfc 8693) to te's token endpoint.
|
||||||
|
// SubjectToken and SubjectTokenType are required parameters.
|
||||||
|
func ExchangeToken(
|
||||||
|
te TokenExchanger,
|
||||||
|
SubjectToken string,
|
||||||
|
SubjectTokenType oidc.TokenType,
|
||||||
|
ActorToken string,
|
||||||
|
ActorTokenType oidc.TokenType,
|
||||||
|
Resource []string,
|
||||||
|
Audience []string,
|
||||||
|
Scopes []string,
|
||||||
|
RequestedTokenType oidc.TokenType,
|
||||||
|
) (*oidc.TokenExchangeResponse, error) {
|
||||||
|
if SubjectToken == "" {
|
||||||
|
return nil, errors.New("empty subject_token")
|
||||||
|
}
|
||||||
|
if SubjectTokenType == "" {
|
||||||
|
return nil, errors.New("empty subject_token_type")
|
||||||
|
}
|
||||||
|
|
||||||
|
authFn, err := te.AuthFn()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
request := oidc.TokenExchangeRequest{
|
||||||
|
GrantType: oidc.GrantTypeTokenExchange,
|
||||||
|
SubjectToken: SubjectToken,
|
||||||
|
SubjectTokenType: SubjectTokenType,
|
||||||
|
ActorToken: ActorToken,
|
||||||
|
ActorTokenType: ActorTokenType,
|
||||||
|
Resource: Resource,
|
||||||
|
Audience: Audience,
|
||||||
|
Scopes: Scopes,
|
||||||
|
RequestedTokenType: RequestedTokenType,
|
||||||
|
}
|
||||||
|
|
||||||
|
return client.CallTokenExchangeEndpoint(request, authFn, te)
|
||||||
|
}
|
|
@ -3,7 +3,7 @@ package oidc
|
||||||
import (
|
import (
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
|
|
||||||
"github.com/zitadel/oidc/pkg/crypto"
|
"github.com/zitadel/oidc/v2/pkg/crypto"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
29
pkg/oidc/device_authorization.go
Normal file
29
pkg/oidc/device_authorization.go
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
package oidc
|
||||||
|
|
||||||
|
// DeviceAuthorizationRequest implements
|
||||||
|
// https://www.rfc-editor.org/rfc/rfc8628#section-3.1,
|
||||||
|
// 3.1 Device Authorization Request.
|
||||||
|
type DeviceAuthorizationRequest struct {
|
||||||
|
Scopes SpaceDelimitedArray `schema:"scope"`
|
||||||
|
ClientID string `schema:"client_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeviceAuthorizationResponse implements
|
||||||
|
// https://www.rfc-editor.org/rfc/rfc8628#section-3.2
|
||||||
|
// 3.2. Device Authorization Response.
|
||||||
|
type DeviceAuthorizationResponse struct {
|
||||||
|
DeviceCode string `json:"device_code"`
|
||||||
|
UserCode string `json:"user_code"`
|
||||||
|
VerificationURI string `json:"verification_uri"`
|
||||||
|
VerificationURIComplete string `json:"verification_uri_complete,omitempty"`
|
||||||
|
ExpiresIn int `json:"expires_in"`
|
||||||
|
Interval int `json:"interval,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeviceAccessTokenRequest implements
|
||||||
|
// https://www.rfc-editor.org/rfc/rfc8628#section-3.4,
|
||||||
|
// Device Access Token Request.
|
||||||
|
type DeviceAccessTokenRequest struct {
|
||||||
|
GrantType GrantType `json:"grant_type" schema:"grant_type"`
|
||||||
|
DeviceCode string `json:"device_code" schema:"device_code"`
|
||||||
|
}
|
|
@ -30,6 +30,8 @@ type DiscoveryConfiguration struct {
|
||||||
// EndSessionEndpoint is a URL where the RP can perform a redirect to request that the End-User be logged out at the OP.
|
// EndSessionEndpoint is a URL where the RP can perform a redirect to request that the End-User be logged out at the OP.
|
||||||
EndSessionEndpoint string `json:"end_session_endpoint,omitempty"`
|
EndSessionEndpoint string `json:"end_session_endpoint,omitempty"`
|
||||||
|
|
||||||
|
DeviceAuthorizationEndpoint string `json:"device_authorization_endpoint,omitempty"`
|
||||||
|
|
||||||
// CheckSessionIframe is a URL where the OP provides an iframe that support cross-origin communications for session state information with the RP Client.
|
// CheckSessionIframe is a URL where the OP provides an iframe that support cross-origin communications for session state information with the RP Client.
|
||||||
CheckSessionIframe string `json:"check_session_iframe,omitempty"`
|
CheckSessionIframe string `json:"check_session_iframe,omitempty"`
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,14 @@ const (
|
||||||
InteractionRequired errorType = "interaction_required"
|
InteractionRequired errorType = "interaction_required"
|
||||||
LoginRequired errorType = "login_required"
|
LoginRequired errorType = "login_required"
|
||||||
RequestNotSupported errorType = "request_not_supported"
|
RequestNotSupported errorType = "request_not_supported"
|
||||||
|
|
||||||
|
// Additional error codes as defined in
|
||||||
|
// https://www.rfc-editor.org/rfc/rfc8628#section-3.5
|
||||||
|
// Device Access Token Response
|
||||||
|
AuthorizationPending errorType = "authorization_pending"
|
||||||
|
SlowDown errorType = "slow_down"
|
||||||
|
AccessDenied errorType = "access_denied"
|
||||||
|
ExpiredToken errorType = "expired_token"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -77,6 +85,32 @@ var (
|
||||||
ErrorType: RequestNotSupported,
|
ErrorType: RequestNotSupported,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Device Access Token errors:
|
||||||
|
ErrAuthorizationPending = func() *Error {
|
||||||
|
return &Error{
|
||||||
|
ErrorType: AuthorizationPending,
|
||||||
|
Description: "The client SHOULD repeat the access token request to the token endpoint, after interval from device authorization response.",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ErrSlowDown = func() *Error {
|
||||||
|
return &Error{
|
||||||
|
ErrorType: SlowDown,
|
||||||
|
Description: "Polling should continue, but the interval MUST be increased by 5 seconds for this and all subsequent requests.",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ErrAccessDenied = func() *Error {
|
||||||
|
return &Error{
|
||||||
|
ErrorType: AccessDenied,
|
||||||
|
Description: "The authorization request was denied.",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ErrExpiredDeviceCode = func() *Error {
|
||||||
|
return &Error{
|
||||||
|
ErrorType: ExpiredToken,
|
||||||
|
Description: "The \"device_code\" has expired.",
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
type Error struct {
|
type Error struct {
|
||||||
|
|
|
@ -1,12 +1,6 @@
|
||||||
package oidc
|
package oidc
|
||||||
|
|
||||||
import (
|
import "github.com/muhlemmer/gu"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"golang.org/x/text/language"
|
|
||||||
)
|
|
||||||
|
|
||||||
type IntrospectionRequest struct {
|
type IntrospectionRequest struct {
|
||||||
Token string `schema:"token"`
|
Token string `schema:"token"`
|
||||||
|
@ -17,36 +11,11 @@ type ClientAssertionParams struct {
|
||||||
ClientAssertionType string `schema:"client_assertion_type"`
|
ClientAssertionType string `schema:"client_assertion_type"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type IntrospectionResponse interface {
|
// IntrospectionResponse implements RFC 7662, section 2.2 and
|
||||||
UserInfoSetter
|
// OpenID Connect Core 1.0, section 5.1 (UserInfo).
|
||||||
IsActive() bool
|
// https://www.rfc-editor.org/rfc/rfc7662.html#section-2.2.
|
||||||
SetActive(bool)
|
// https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims.
|
||||||
SetScopes(scopes []string)
|
type IntrospectionResponse struct {
|
||||||
SetClientID(id string)
|
|
||||||
SetTokenType(tokenType string)
|
|
||||||
SetExpiration(exp time.Time)
|
|
||||||
SetIssuedAt(iat time.Time)
|
|
||||||
SetNotBefore(nbf time.Time)
|
|
||||||
SetAudience(audience []string)
|
|
||||||
SetIssuer(issuer string)
|
|
||||||
SetJWTID(id string)
|
|
||||||
GetScope() []string
|
|
||||||
GetClientID() string
|
|
||||||
GetTokenType() string
|
|
||||||
GetExpiration() time.Time
|
|
||||||
GetIssuedAt() time.Time
|
|
||||||
GetNotBefore() time.Time
|
|
||||||
GetSubject() string
|
|
||||||
GetAudience() []string
|
|
||||||
GetIssuer() string
|
|
||||||
GetJWTID() string
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewIntrospectionResponse() IntrospectionResponse {
|
|
||||||
return &introspectionResponse{}
|
|
||||||
}
|
|
||||||
|
|
||||||
type introspectionResponse struct {
|
|
||||||
Active bool `json:"active"`
|
Active bool `json:"active"`
|
||||||
Scope SpaceDelimitedArray `json:"scope,omitempty"`
|
Scope SpaceDelimitedArray `json:"scope,omitempty"`
|
||||||
ClientID string `json:"client_id,omitempty"`
|
ClientID string `json:"client_id,omitempty"`
|
||||||
|
@ -58,323 +27,50 @@ type introspectionResponse struct {
|
||||||
Audience Audience `json:"aud,omitempty"`
|
Audience Audience `json:"aud,omitempty"`
|
||||||
Issuer string `json:"iss,omitempty"`
|
Issuer string `json:"iss,omitempty"`
|
||||||
JWTID string `json:"jti,omitempty"`
|
JWTID string `json:"jti,omitempty"`
|
||||||
userInfoProfile
|
Username string `json:"username,omitempty"`
|
||||||
userInfoEmail
|
UserInfoProfile
|
||||||
userInfoPhone
|
UserInfoEmail
|
||||||
|
UserInfoPhone
|
||||||
|
|
||||||
Address UserInfoAddress `json:"address,omitempty"`
|
Address *UserInfoAddress `json:"address,omitempty"`
|
||||||
claims map[string]interface{}
|
Claims map[string]any `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *introspectionResponse) IsActive() bool {
|
// SetUserInfo copies all relevant fields from UserInfo
|
||||||
return i.Active
|
// into the IntroSpectionResponse.
|
||||||
|
func (i *IntrospectionResponse) SetUserInfo(u *UserInfo) {
|
||||||
|
i.Subject = u.Subject
|
||||||
|
i.Username = u.PreferredUsername
|
||||||
|
i.Address = gu.PtrCopy(u.Address)
|
||||||
|
i.UserInfoProfile = u.UserInfoProfile
|
||||||
|
i.UserInfoEmail = u.UserInfoEmail
|
||||||
|
i.UserInfoPhone = u.UserInfoPhone
|
||||||
|
if i.Claims == nil {
|
||||||
|
i.Claims = gu.MapCopy(u.Claims)
|
||||||
|
} else {
|
||||||
|
gu.MapMerge(u.Claims, i.Claims)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *introspectionResponse) GetSubject() string {
|
// GetAddress is a safe getter that takes
|
||||||
return i.Subject
|
// care of a possible nil value.
|
||||||
}
|
func (i *IntrospectionResponse) GetAddress() *UserInfoAddress {
|
||||||
|
if i.Address == nil {
|
||||||
func (i *introspectionResponse) GetName() string {
|
return new(UserInfoAddress)
|
||||||
return i.Name
|
}
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) GetGivenName() string {
|
|
||||||
return i.GivenName
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) GetFamilyName() string {
|
|
||||||
return i.FamilyName
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) GetMiddleName() string {
|
|
||||||
return i.MiddleName
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) GetNickname() string {
|
|
||||||
return i.Nickname
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) GetProfile() string {
|
|
||||||
return i.Profile
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) GetPicture() string {
|
|
||||||
return i.Picture
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) GetWebsite() string {
|
|
||||||
return i.Website
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) GetGender() Gender {
|
|
||||||
return i.Gender
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) GetBirthdate() string {
|
|
||||||
return i.Birthdate
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) GetZoneinfo() string {
|
|
||||||
return i.Zoneinfo
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) GetLocale() language.Tag {
|
|
||||||
return i.Locale
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) GetPreferredUsername() string {
|
|
||||||
return i.PreferredUsername
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) GetEmail() string {
|
|
||||||
return i.Email
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) IsEmailVerified() bool {
|
|
||||||
return bool(i.EmailVerified)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) GetPhoneNumber() string {
|
|
||||||
return i.PhoneNumber
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) IsPhoneNumberVerified() bool {
|
|
||||||
return i.PhoneNumberVerified
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) GetAddress() UserInfoAddress {
|
|
||||||
return i.Address
|
return i.Address
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *introspectionResponse) GetClaim(key string) interface{} {
|
// introspectionResponseAlias prevents loops on the JSON methods
|
||||||
return i.claims[key]
|
type introspectionResponseAlias IntrospectionResponse
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) GetClaims() map[string]interface{} {
|
func (i *IntrospectionResponse) MarshalJSON() ([]byte, error) {
|
||||||
return i.claims
|
if i.Username == "" {
|
||||||
}
|
i.Username = i.PreferredUsername
|
||||||
|
|
||||||
func (i *introspectionResponse) GetScope() []string {
|
|
||||||
return []string(i.Scope)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) GetClientID() string {
|
|
||||||
return i.ClientID
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) GetTokenType() string {
|
|
||||||
return i.TokenType
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) GetExpiration() time.Time {
|
|
||||||
return time.Time(i.Expiration)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) GetIssuedAt() time.Time {
|
|
||||||
return time.Time(i.IssuedAt)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) GetNotBefore() time.Time {
|
|
||||||
return time.Time(i.NotBefore)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) GetAudience() []string {
|
|
||||||
return []string(i.Audience)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) GetIssuer() string {
|
|
||||||
return i.Issuer
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) GetJWTID() string {
|
|
||||||
return i.JWTID
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) SetActive(active bool) {
|
|
||||||
i.Active = active
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) SetScopes(scope []string) {
|
|
||||||
i.Scope = scope
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) SetClientID(id string) {
|
|
||||||
i.ClientID = id
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) SetTokenType(tokenType string) {
|
|
||||||
i.TokenType = tokenType
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) SetExpiration(exp time.Time) {
|
|
||||||
i.Expiration = Time(exp)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) SetIssuedAt(iat time.Time) {
|
|
||||||
i.IssuedAt = Time(iat)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) SetNotBefore(nbf time.Time) {
|
|
||||||
i.NotBefore = Time(nbf)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) SetAudience(audience []string) {
|
|
||||||
i.Audience = audience
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) SetIssuer(issuer string) {
|
|
||||||
i.Issuer = issuer
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) SetJWTID(id string) {
|
|
||||||
i.JWTID = id
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) SetSubject(sub string) {
|
|
||||||
i.Subject = sub
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) SetName(name string) {
|
|
||||||
i.Name = name
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) SetGivenName(name string) {
|
|
||||||
i.GivenName = name
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) SetFamilyName(name string) {
|
|
||||||
i.FamilyName = name
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) SetMiddleName(name string) {
|
|
||||||
i.MiddleName = name
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) SetNickname(name string) {
|
|
||||||
i.Nickname = name
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) SetUpdatedAt(date time.Time) {
|
|
||||||
i.UpdatedAt = Time(date)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) SetProfile(profile string) {
|
|
||||||
i.Profile = profile
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) SetPicture(picture string) {
|
|
||||||
i.Picture = picture
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) SetWebsite(website string) {
|
|
||||||
i.Website = website
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) SetGender(gender Gender) {
|
|
||||||
i.Gender = gender
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) SetBirthdate(birthdate string) {
|
|
||||||
i.Birthdate = birthdate
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) SetZoneinfo(zoneInfo string) {
|
|
||||||
i.Zoneinfo = zoneInfo
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) SetLocale(locale language.Tag) {
|
|
||||||
i.Locale = locale
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) SetPreferredUsername(name string) {
|
|
||||||
i.PreferredUsername = name
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) SetEmail(email string, verified bool) {
|
|
||||||
i.Email = email
|
|
||||||
i.EmailVerified = boolString(verified)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) SetPhone(phone string, verified bool) {
|
|
||||||
i.PhoneNumber = phone
|
|
||||||
i.PhoneNumberVerified = verified
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) SetAddress(address UserInfoAddress) {
|
|
||||||
i.Address = address
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) AppendClaims(key string, value interface{}) {
|
|
||||||
if i.claims == nil {
|
|
||||||
i.claims = make(map[string]interface{})
|
|
||||||
}
|
}
|
||||||
i.claims[key] = value
|
return mergeAndMarshalClaims((*introspectionResponseAlias)(i), i.Claims)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *introspectionResponse) MarshalJSON() ([]byte, error) {
|
func (i *IntrospectionResponse) UnmarshalJSON(data []byte) error {
|
||||||
type Alias introspectionResponse
|
return unmarshalJSONMulti(data, (*introspectionResponseAlias)(i), &i.Claims)
|
||||||
a := &struct {
|
|
||||||
*Alias
|
|
||||||
Expiration int64 `json:"exp,omitempty"`
|
|
||||||
IssuedAt int64 `json:"iat,omitempty"`
|
|
||||||
NotBefore int64 `json:"nbf,omitempty"`
|
|
||||||
Locale interface{} `json:"locale,omitempty"`
|
|
||||||
UpdatedAt int64 `json:"updated_at,omitempty"`
|
|
||||||
Username string `json:"username,omitempty"`
|
|
||||||
}{
|
|
||||||
Alias: (*Alias)(i),
|
|
||||||
}
|
|
||||||
if !i.Locale.IsRoot() {
|
|
||||||
a.Locale = i.Locale
|
|
||||||
}
|
|
||||||
if !time.Time(i.UpdatedAt).IsZero() {
|
|
||||||
a.UpdatedAt = time.Time(i.UpdatedAt).Unix()
|
|
||||||
}
|
|
||||||
if !time.Time(i.Expiration).IsZero() {
|
|
||||||
a.Expiration = time.Time(i.Expiration).Unix()
|
|
||||||
}
|
|
||||||
if !time.Time(i.IssuedAt).IsZero() {
|
|
||||||
a.IssuedAt = time.Time(i.IssuedAt).Unix()
|
|
||||||
}
|
|
||||||
if !time.Time(i.NotBefore).IsZero() {
|
|
||||||
a.NotBefore = time.Time(i.NotBefore).Unix()
|
|
||||||
}
|
|
||||||
a.Username = i.PreferredUsername
|
|
||||||
|
|
||||||
b, err := json.Marshal(a)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(i.claims) == 0 {
|
|
||||||
return b, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
err = json.Unmarshal(b, &i.claims)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("jws: invalid map of custom claims %v", i.claims)
|
|
||||||
}
|
|
||||||
|
|
||||||
return json.Marshal(i.claims)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) UnmarshalJSON(data []byte) error {
|
|
||||||
type Alias introspectionResponse
|
|
||||||
a := &struct {
|
|
||||||
*Alias
|
|
||||||
UpdatedAt int64 `json:"update_at,omitempty"`
|
|
||||||
}{
|
|
||||||
Alias: (*Alias)(i),
|
|
||||||
}
|
|
||||||
if err := json.Unmarshal(data, &a); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
i.UpdatedAt = Time(time.Unix(a.UpdatedAt, 0).UTC())
|
|
||||||
|
|
||||||
if err := json.Unmarshal(data, &i.claims); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
78
pkg/oidc/introspection_test.go
Normal file
78
pkg/oidc/introspection_test.go
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
package oidc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestIntrospectionResponse_SetUserInfo(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
start *IntrospectionResponse
|
||||||
|
want *IntrospectionResponse
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
|
||||||
|
name: "nil claims",
|
||||||
|
start: &IntrospectionResponse{},
|
||||||
|
want: &IntrospectionResponse{
|
||||||
|
Subject: userInfoData.Subject,
|
||||||
|
Username: userInfoData.PreferredUsername,
|
||||||
|
Address: userInfoData.Address,
|
||||||
|
UserInfoProfile: userInfoData.UserInfoProfile,
|
||||||
|
UserInfoEmail: userInfoData.UserInfoEmail,
|
||||||
|
UserInfoPhone: userInfoData.UserInfoPhone,
|
||||||
|
Claims: userInfoData.Claims,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
|
||||||
|
name: "merge claims",
|
||||||
|
start: &IntrospectionResponse{
|
||||||
|
Claims: map[string]any{
|
||||||
|
"hello": "world",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: &IntrospectionResponse{
|
||||||
|
Subject: userInfoData.Subject,
|
||||||
|
Username: userInfoData.PreferredUsername,
|
||||||
|
Address: userInfoData.Address,
|
||||||
|
UserInfoProfile: userInfoData.UserInfoProfile,
|
||||||
|
UserInfoEmail: userInfoData.UserInfoEmail,
|
||||||
|
UserInfoPhone: userInfoData.UserInfoPhone,
|
||||||
|
Claims: map[string]any{
|
||||||
|
"foo": "bar",
|
||||||
|
"hello": "world",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
tt.start.SetUserInfo(userInfoData)
|
||||||
|
assert.Equal(t, tt.want, tt.start)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIntrospectionResponse_GetAddress(t *testing.T) {
|
||||||
|
// nil address
|
||||||
|
i := new(IntrospectionResponse)
|
||||||
|
assert.Equal(t, &UserInfoAddress{}, i.GetAddress())
|
||||||
|
|
||||||
|
i.Address = &UserInfoAddress{PostalCode: "1234"}
|
||||||
|
assert.Equal(t, i.Address, i.GetAddress())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIntrospectionResponse_MarshalJSON(t *testing.T) {
|
||||||
|
got, err := json.Marshal(&IntrospectionResponse{
|
||||||
|
UserInfoProfile: UserInfoProfile{
|
||||||
|
PreferredUsername: "muhlemmer",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, string(got), `{"active":false,"username":"muhlemmer","preferred_username":"muhlemmer"}`)
|
||||||
|
}
|
50
pkg/oidc/regression_assert_test.go
Normal file
50
pkg/oidc/regression_assert_test.go
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
//go:build !create_regression_data
|
||||||
|
|
||||||
|
package oidc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Test_assert_regression verifies current output from
|
||||||
|
// json.Marshal to stored regression data.
|
||||||
|
// These tests are only ran when the create_regression_data
|
||||||
|
// tag is NOT set.
|
||||||
|
func Test_assert_regression(t *testing.T) {
|
||||||
|
buf := new(strings.Builder)
|
||||||
|
|
||||||
|
for _, obj := range regressionData {
|
||||||
|
name := jsonFilename(obj)
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
file, err := os.Open(name)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
_, err = io.Copy(buf, file)
|
||||||
|
require.NoError(t, err)
|
||||||
|
want := buf.String()
|
||||||
|
buf.Reset()
|
||||||
|
|
||||||
|
encodeJSON(t, buf, obj)
|
||||||
|
first := buf.String()
|
||||||
|
buf.Reset()
|
||||||
|
|
||||||
|
assert.JSONEq(t, want, first)
|
||||||
|
|
||||||
|
require.NoError(t,
|
||||||
|
json.Unmarshal([]byte(first), obj),
|
||||||
|
)
|
||||||
|
second, err := json.Marshal(obj)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.JSONEq(t, want, string(second))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
24
pkg/oidc/regression_create_test.go
Normal file
24
pkg/oidc/regression_create_test.go
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
//go:build create_regression_data
|
||||||
|
|
||||||
|
package oidc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Test_create_regression generates the regression data.
|
||||||
|
// It is excluded from regular testing, unless
|
||||||
|
// called with the create_regression_data tag:
|
||||||
|
// go test -tags="create_regression_data" ./pkg/oidc
|
||||||
|
func Test_create_regression(t *testing.T) {
|
||||||
|
for _, obj := range regressionData {
|
||||||
|
file, err := os.Create(jsonFilename(obj))
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
encodeJSON(t, file, obj)
|
||||||
|
}
|
||||||
|
}
|
23
pkg/oidc/regression_data/oidc.AccessTokenClaims.json
Normal file
23
pkg/oidc/regression_data/oidc.AccessTokenClaims.json
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
{
|
||||||
|
"iss": "zitadel",
|
||||||
|
"sub": "hello@me.com",
|
||||||
|
"aud": [
|
||||||
|
"foo",
|
||||||
|
"bar"
|
||||||
|
],
|
||||||
|
"jti": "900",
|
||||||
|
"azp": "just@me.com",
|
||||||
|
"nonce": "6969",
|
||||||
|
"acr": "something",
|
||||||
|
"amr": [
|
||||||
|
"some",
|
||||||
|
"methods"
|
||||||
|
],
|
||||||
|
"scope": "email phone",
|
||||||
|
"client_id": "777",
|
||||||
|
"exp": 12345,
|
||||||
|
"iat": 12000,
|
||||||
|
"nbf": 12000,
|
||||||
|
"auth_time": 12000,
|
||||||
|
"foo": "bar"
|
||||||
|
}
|
51
pkg/oidc/regression_data/oidc.IDTokenClaims.json
Normal file
51
pkg/oidc/regression_data/oidc.IDTokenClaims.json
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
{
|
||||||
|
"iss": "zitadel",
|
||||||
|
"aud": [
|
||||||
|
"foo",
|
||||||
|
"bar"
|
||||||
|
],
|
||||||
|
"jti": "900",
|
||||||
|
"azp": "just@me.com",
|
||||||
|
"nonce": "6969",
|
||||||
|
"at_hash": "acthashhash",
|
||||||
|
"c_hash": "hashhash",
|
||||||
|
"acr": "something",
|
||||||
|
"amr": [
|
||||||
|
"some",
|
||||||
|
"methods"
|
||||||
|
],
|
||||||
|
"sid": "666",
|
||||||
|
"client_id": "777",
|
||||||
|
"exp": 12345,
|
||||||
|
"iat": 12000,
|
||||||
|
"nbf": 12000,
|
||||||
|
"auth_time": 12000,
|
||||||
|
"address": {
|
||||||
|
"country": "Moon",
|
||||||
|
"formatted": "Sesame street 666\n666-666, Smallvile\nMoon",
|
||||||
|
"locality": "Smallvile",
|
||||||
|
"postal_code": "666-666",
|
||||||
|
"region": "Outer space",
|
||||||
|
"street_address": "Sesame street 666"
|
||||||
|
},
|
||||||
|
"birthdate": "1st of April",
|
||||||
|
"email": "tim@zitadel.com",
|
||||||
|
"email_verified": true,
|
||||||
|
"family_name": "Möhlmann",
|
||||||
|
"foo": "bar",
|
||||||
|
"gender": "male",
|
||||||
|
"given_name": "Tim",
|
||||||
|
"locale": "nl",
|
||||||
|
"middle_name": "Danger",
|
||||||
|
"name": "Tim Möhlmann",
|
||||||
|
"nickname": "muhlemmer",
|
||||||
|
"phone_number": "+1234567890",
|
||||||
|
"phone_number_verified": true,
|
||||||
|
"picture": "https://avatars.githubusercontent.com/u/5411563?v=4",
|
||||||
|
"preferred_username": "muhlemmer",
|
||||||
|
"profile": "https://github.com/muhlemmer",
|
||||||
|
"sub": "hello@me.com",
|
||||||
|
"updated_at": 1,
|
||||||
|
"website": "https://zitadel.com",
|
||||||
|
"zoneinfo": "Europe/Amsterdam"
|
||||||
|
}
|
44
pkg/oidc/regression_data/oidc.IntrospectionResponse.json
Normal file
44
pkg/oidc/regression_data/oidc.IntrospectionResponse.json
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
{
|
||||||
|
"active": true,
|
||||||
|
"address": {
|
||||||
|
"country": "Moon",
|
||||||
|
"formatted": "Sesame street 666\n666-666, Smallvile\nMoon",
|
||||||
|
"locality": "Smallvile",
|
||||||
|
"postal_code": "666-666",
|
||||||
|
"region": "Outer space",
|
||||||
|
"street_address": "Sesame street 666"
|
||||||
|
},
|
||||||
|
"aud": [
|
||||||
|
"foo",
|
||||||
|
"bar"
|
||||||
|
],
|
||||||
|
"birthdate": "1st of April",
|
||||||
|
"client_id": "777",
|
||||||
|
"email": "tim@zitadel.com",
|
||||||
|
"email_verified": true,
|
||||||
|
"exp": 12345,
|
||||||
|
"family_name": "Möhlmann",
|
||||||
|
"foo": "bar",
|
||||||
|
"gender": "male",
|
||||||
|
"given_name": "Tim",
|
||||||
|
"iat": 12000,
|
||||||
|
"iss": "zitadel",
|
||||||
|
"jti": "900",
|
||||||
|
"locale": "nl",
|
||||||
|
"middle_name": "Danger",
|
||||||
|
"name": "Tim Möhlmann",
|
||||||
|
"nbf": 12000,
|
||||||
|
"nickname": "muhlemmer",
|
||||||
|
"phone_number": "+1234567890",
|
||||||
|
"phone_number_verified": true,
|
||||||
|
"picture": "https://avatars.githubusercontent.com/u/5411563?v=4",
|
||||||
|
"preferred_username": "muhlemmer",
|
||||||
|
"profile": "https://github.com/muhlemmer",
|
||||||
|
"scope": "email phone",
|
||||||
|
"sub": "hello@me.com",
|
||||||
|
"token_type": "idtoken",
|
||||||
|
"updated_at": 1,
|
||||||
|
"username": "muhlemmer",
|
||||||
|
"website": "https://zitadel.com",
|
||||||
|
"zoneinfo": "Europe/Amsterdam"
|
||||||
|
}
|
11
pkg/oidc/regression_data/oidc.JWTProfileAssertionClaims.json
Normal file
11
pkg/oidc/regression_data/oidc.JWTProfileAssertionClaims.json
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"aud": [
|
||||||
|
"foo",
|
||||||
|
"bar"
|
||||||
|
],
|
||||||
|
"exp": 12345,
|
||||||
|
"foo": "bar",
|
||||||
|
"iat": 12000,
|
||||||
|
"iss": "zitadel",
|
||||||
|
"sub": "hello@me.com"
|
||||||
|
}
|
30
pkg/oidc/regression_data/oidc.UserInfo.json
Normal file
30
pkg/oidc/regression_data/oidc.UserInfo.json
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
{
|
||||||
|
"address": {
|
||||||
|
"country": "Moon",
|
||||||
|
"formatted": "Sesame street 666\n666-666, Smallvile\nMoon",
|
||||||
|
"locality": "Smallvile",
|
||||||
|
"postal_code": "666-666",
|
||||||
|
"region": "Outer space",
|
||||||
|
"street_address": "Sesame street 666"
|
||||||
|
},
|
||||||
|
"birthdate": "1st of April",
|
||||||
|
"email": "tim@zitadel.com",
|
||||||
|
"email_verified": true,
|
||||||
|
"family_name": "Möhlmann",
|
||||||
|
"foo": "bar",
|
||||||
|
"gender": "male",
|
||||||
|
"given_name": "Tim",
|
||||||
|
"locale": "nl",
|
||||||
|
"middle_name": "Danger",
|
||||||
|
"name": "Tim Möhlmann",
|
||||||
|
"nickname": "muhlemmer",
|
||||||
|
"phone_number": "+1234567890",
|
||||||
|
"phone_number_verified": true,
|
||||||
|
"picture": "https://avatars.githubusercontent.com/u/5411563?v=4",
|
||||||
|
"preferred_username": "muhlemmer",
|
||||||
|
"profile": "https://github.com/muhlemmer",
|
||||||
|
"sub": "hello@me.com",
|
||||||
|
"updated_at": 1,
|
||||||
|
"website": "https://zitadel.com",
|
||||||
|
"zoneinfo": "Europe/Amsterdam"
|
||||||
|
}
|
40
pkg/oidc/regression_test.go
Normal file
40
pkg/oidc/regression_test.go
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
package oidc
|
||||||
|
|
||||||
|
// This file contains common functions and data for regression testing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
const dataDir = "regression_data"
|
||||||
|
|
||||||
|
// jsonFilename builds a filename for the regression testdata.
|
||||||
|
// dataDir/<type_name>.json
|
||||||
|
func jsonFilename(obj interface{}) string {
|
||||||
|
name := fmt.Sprintf("%T.json", obj)
|
||||||
|
return path.Join(
|
||||||
|
dataDir,
|
||||||
|
strings.TrimPrefix(name, "*"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodeJSON(t *testing.T, w io.Writer, obj interface{}) {
|
||||||
|
enc := json.NewEncoder(w)
|
||||||
|
enc.SetIndent("", "\t")
|
||||||
|
require.NoError(t, enc.Encode(obj))
|
||||||
|
}
|
||||||
|
|
||||||
|
var regressionData = []interface{}{
|
||||||
|
accessTokenData,
|
||||||
|
idTokenData,
|
||||||
|
introspectionResponseData,
|
||||||
|
userInfoData,
|
||||||
|
jwtProfileAssertionData,
|
||||||
|
}
|
|
@ -2,15 +2,13 @@ package oidc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"os"
|
||||||
"io/ioutil"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
"gopkg.in/square/go-jose.v2"
|
"gopkg.in/square/go-jose.v2"
|
||||||
|
|
||||||
"github.com/zitadel/oidc/pkg/crypto"
|
"github.com/zitadel/oidc/v2/pkg/crypto"
|
||||||
"github.com/zitadel/oidc/pkg/http"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -20,374 +18,174 @@ const (
|
||||||
PrefixBearer = BearerToken + " "
|
PrefixBearer = BearerToken + " "
|
||||||
)
|
)
|
||||||
|
|
||||||
type Tokens struct {
|
type Tokens[C IDClaims] struct {
|
||||||
*oauth2.Token
|
*oauth2.Token
|
||||||
IDTokenClaims IDTokenClaims
|
IDTokenClaims C
|
||||||
IDToken string
|
IDToken string
|
||||||
}
|
}
|
||||||
|
|
||||||
type AccessTokenClaims interface {
|
// TokenClaims contains the base Claims used all tokens.
|
||||||
Claims
|
// It implements OpenID Connect Core 1.0, section 2.
|
||||||
GetSubject() string
|
// https://openid.net/specs/openid-connect-core-1_0.html#IDToken
|
||||||
GetTokenID() string
|
// And RFC 9068: JSON Web Token (JWT) Profile for OAuth 2.0 Access Tokens,
|
||||||
SetPrivateClaims(map[string]interface{})
|
// section 2.2. https://datatracker.ietf.org/doc/html/rfc9068#name-data-structure
|
||||||
}
|
//
|
||||||
|
// TokenClaims implements the Claims interface,
|
||||||
type IDTokenClaims interface {
|
// and can be used to extend larger claim types by embedding.
|
||||||
Claims
|
type TokenClaims struct {
|
||||||
GetNotBefore() time.Time
|
|
||||||
GetJWTID() string
|
|
||||||
GetAccessTokenHash() string
|
|
||||||
GetCodeHash() string
|
|
||||||
GetAuthenticationMethodsReferences() []string
|
|
||||||
GetClientID() string
|
|
||||||
GetSignatureAlgorithm() jose.SignatureAlgorithm
|
|
||||||
SetAccessTokenHash(hash string)
|
|
||||||
SetUserinfo(userinfo UserInfo)
|
|
||||||
SetCodeHash(hash string)
|
|
||||||
UserInfo
|
|
||||||
}
|
|
||||||
|
|
||||||
func EmptyAccessTokenClaims() AccessTokenClaims {
|
|
||||||
return new(accessTokenClaims)
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewAccessTokenClaims(issuer, subject string, audience []string, expiration time.Time, id, clientID string, skew time.Duration) AccessTokenClaims {
|
|
||||||
now := time.Now().UTC().Add(-skew)
|
|
||||||
if len(audience) == 0 {
|
|
||||||
audience = append(audience, clientID)
|
|
||||||
}
|
|
||||||
return &accessTokenClaims{
|
|
||||||
Issuer: issuer,
|
|
||||||
Subject: subject,
|
|
||||||
Audience: audience,
|
|
||||||
Expiration: Time(expiration),
|
|
||||||
IssuedAt: Time(now),
|
|
||||||
NotBefore: Time(now),
|
|
||||||
JWTID: id,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type accessTokenClaims struct {
|
|
||||||
Issuer string `json:"iss,omitempty"`
|
Issuer string `json:"iss,omitempty"`
|
||||||
Subject string `json:"sub,omitempty"`
|
Subject string `json:"sub,omitempty"`
|
||||||
Audience Audience `json:"aud,omitempty"`
|
Audience Audience `json:"aud,omitempty"`
|
||||||
Expiration Time `json:"exp,omitempty"`
|
Expiration Time `json:"exp,omitempty"`
|
||||||
IssuedAt Time `json:"iat,omitempty"`
|
IssuedAt Time `json:"iat,omitempty"`
|
||||||
NotBefore Time `json:"nbf,omitempty"`
|
|
||||||
JWTID string `json:"jti,omitempty"`
|
|
||||||
AuthorizedParty string `json:"azp,omitempty"`
|
|
||||||
Nonce string `json:"nonce,omitempty"`
|
|
||||||
AuthTime Time `json:"auth_time,omitempty"`
|
AuthTime Time `json:"auth_time,omitempty"`
|
||||||
CodeHash string `json:"c_hash,omitempty"`
|
NotBefore Time `json:"nbf,omitempty"`
|
||||||
|
Nonce string `json:"nonce,omitempty"`
|
||||||
AuthenticationContextClassReference string `json:"acr,omitempty"`
|
AuthenticationContextClassReference string `json:"acr,omitempty"`
|
||||||
AuthenticationMethodsReferences []string `json:"amr,omitempty"`
|
AuthenticationMethodsReferences []string `json:"amr,omitempty"`
|
||||||
SessionID string `json:"sid,omitempty"`
|
AuthorizedParty string `json:"azp,omitempty"`
|
||||||
Scopes SpaceDelimitedArray `json:"scope,omitempty"`
|
|
||||||
ClientID string `json:"client_id,omitempty"`
|
ClientID string `json:"client_id,omitempty"`
|
||||||
AccessTokenUseNumber int `json:"at_use_nbr,omitempty"`
|
JWTID string `json:"jti,omitempty"`
|
||||||
|
|
||||||
claims map[string]interface{} `json:"-"`
|
// Additional information set by this framework
|
||||||
signatureAlg jose.SignatureAlgorithm `json:"-"`
|
SignatureAlg jose.SignatureAlgorithm `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetIssuer implements the Claims interface
|
func (c *TokenClaims) GetIssuer() string {
|
||||||
func (a *accessTokenClaims) GetIssuer() string {
|
return c.Issuer
|
||||||
return a.Issuer
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAudience implements the Claims interface
|
func (c *TokenClaims) GetSubject() string {
|
||||||
func (a *accessTokenClaims) GetAudience() []string {
|
return c.Subject
|
||||||
return a.Audience
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetExpiration implements the Claims interface
|
func (c *TokenClaims) GetAudience() []string {
|
||||||
func (a *accessTokenClaims) GetExpiration() time.Time {
|
return c.Audience
|
||||||
return time.Time(a.Expiration)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetIssuedAt implements the Claims interface
|
func (c *TokenClaims) GetExpiration() time.Time {
|
||||||
func (a *accessTokenClaims) GetIssuedAt() time.Time {
|
return c.Expiration.AsTime()
|
||||||
return time.Time(a.IssuedAt)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetNonce implements the Claims interface
|
func (c *TokenClaims) GetIssuedAt() time.Time {
|
||||||
func (a *accessTokenClaims) GetNonce() string {
|
return c.IssuedAt.AsTime()
|
||||||
return a.Nonce
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAuthenticationContextClassReference implements the Claims interface
|
func (c *TokenClaims) GetNonce() string {
|
||||||
func (a *accessTokenClaims) GetAuthenticationContextClassReference() string {
|
return c.Nonce
|
||||||
return a.AuthenticationContextClassReference
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAuthTime implements the Claims interface
|
func (c *TokenClaims) GetAuthTime() time.Time {
|
||||||
func (a *accessTokenClaims) GetAuthTime() time.Time {
|
return c.AuthTime.AsTime()
|
||||||
return time.Time(a.AuthTime)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAuthorizedParty implements the Claims interface
|
func (c *TokenClaims) GetAuthorizedParty() string {
|
||||||
func (a *accessTokenClaims) GetAuthorizedParty() string {
|
return c.AuthorizedParty
|
||||||
return a.AuthorizedParty
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetSignatureAlgorithm implements the Claims interface
|
func (c *TokenClaims) GetSignatureAlgorithm() jose.SignatureAlgorithm {
|
||||||
func (a *accessTokenClaims) SetSignatureAlgorithm(algorithm jose.SignatureAlgorithm) {
|
return c.SignatureAlg
|
||||||
a.signatureAlg = algorithm
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetSubject implements the AccessTokenClaims interface
|
func (c *TokenClaims) GetAuthenticationContextClassReference() string {
|
||||||
func (a *accessTokenClaims) GetSubject() string {
|
return c.AuthenticationContextClassReference
|
||||||
return a.Subject
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetTokenID implements the AccessTokenClaims interface
|
func (c *TokenClaims) SetSignatureAlgorithm(algorithm jose.SignatureAlgorithm) {
|
||||||
func (a *accessTokenClaims) GetTokenID() string {
|
c.SignatureAlg = algorithm
|
||||||
return a.JWTID
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetPrivateClaims implements the AccessTokenClaims interface
|
type AccessTokenClaims struct {
|
||||||
func (a *accessTokenClaims) SetPrivateClaims(claims map[string]interface{}) {
|
TokenClaims
|
||||||
a.claims = claims
|
Scopes SpaceDelimitedArray `json:"scope,omitempty"`
|
||||||
|
Claims map[string]any `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *accessTokenClaims) MarshalJSON() ([]byte, error) {
|
func NewAccessTokenClaims(issuer, subject string, audience []string, expiration time.Time, jwtid, clientID string, skew time.Duration) *AccessTokenClaims {
|
||||||
type Alias accessTokenClaims
|
now := time.Now().UTC().Add(-skew)
|
||||||
s := &struct {
|
if len(audience) == 0 {
|
||||||
*Alias
|
audience = append(audience, clientID)
|
||||||
Expiration int64 `json:"exp,omitempty"`
|
|
||||||
IssuedAt int64 `json:"iat,omitempty"`
|
|
||||||
NotBefore int64 `json:"nbf,omitempty"`
|
|
||||||
AuthTime int64 `json:"auth_time,omitempty"`
|
|
||||||
}{
|
|
||||||
Alias: (*Alias)(a),
|
|
||||||
}
|
}
|
||||||
if !time.Time(a.Expiration).IsZero() {
|
return &AccessTokenClaims{
|
||||||
s.Expiration = time.Time(a.Expiration).Unix()
|
TokenClaims: TokenClaims{
|
||||||
}
|
|
||||||
if !time.Time(a.IssuedAt).IsZero() {
|
|
||||||
s.IssuedAt = time.Time(a.IssuedAt).Unix()
|
|
||||||
}
|
|
||||||
if !time.Time(a.NotBefore).IsZero() {
|
|
||||||
s.NotBefore = time.Time(a.NotBefore).Unix()
|
|
||||||
}
|
|
||||||
if !time.Time(a.AuthTime).IsZero() {
|
|
||||||
s.AuthTime = time.Time(a.AuthTime).Unix()
|
|
||||||
}
|
|
||||||
b, err := json.Marshal(s)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if a.claims == nil {
|
|
||||||
return b, nil
|
|
||||||
}
|
|
||||||
info, err := json.Marshal(a.claims)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return http.ConcatenateJSON(b, info)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *accessTokenClaims) UnmarshalJSON(data []byte) error {
|
|
||||||
type Alias accessTokenClaims
|
|
||||||
if err := json.Unmarshal(data, (*Alias)(a)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
claims := make(map[string]interface{})
|
|
||||||
if err := json.Unmarshal(data, &claims); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
a.claims = claims
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func EmptyIDTokenClaims() IDTokenClaims {
|
|
||||||
return new(idTokenClaims)
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewIDTokenClaims(issuer, subject string, audience []string, expiration, authTime time.Time, nonce string, acr string, amr []string, clientID string, skew time.Duration) IDTokenClaims {
|
|
||||||
audience = AppendClientIDToAudience(clientID, audience)
|
|
||||||
return &idTokenClaims{
|
|
||||||
Issuer: issuer,
|
Issuer: issuer,
|
||||||
|
Subject: subject,
|
||||||
Audience: audience,
|
Audience: audience,
|
||||||
Expiration: Time(expiration),
|
Expiration: FromTime(expiration),
|
||||||
IssuedAt: Time(time.Now().UTC().Add(-skew)),
|
IssuedAt: FromTime(now),
|
||||||
AuthTime: Time(authTime.Add(-skew)),
|
NotBefore: FromTime(now),
|
||||||
|
JWTID: jwtid,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type atcAlias AccessTokenClaims
|
||||||
|
|
||||||
|
func (a *AccessTokenClaims) MarshalJSON() ([]byte, error) {
|
||||||
|
return mergeAndMarshalClaims((*atcAlias)(a), a.Claims)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AccessTokenClaims) UnmarshalJSON(data []byte) error {
|
||||||
|
return unmarshalJSONMulti(data, (*atcAlias)(a), &a.Claims)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IDTokenClaims extends TokenClaims by further implementing
|
||||||
|
// OpenID Connect Core 1.0, sections 3.1.3.6 (Code flow),
|
||||||
|
// 3.2.2.10 (implicit), 3.3.2.11 (Hybrid) and 5.1 (UserInfo).
|
||||||
|
// https://openid.net/specs/openid-connect-core-1_0.html#toc
|
||||||
|
type IDTokenClaims struct {
|
||||||
|
TokenClaims
|
||||||
|
NotBefore Time `json:"nbf,omitempty"`
|
||||||
|
AccessTokenHash string `json:"at_hash,omitempty"`
|
||||||
|
CodeHash string `json:"c_hash,omitempty"`
|
||||||
|
SessionID string `json:"sid,omitempty"`
|
||||||
|
UserInfoProfile
|
||||||
|
UserInfoEmail
|
||||||
|
UserInfoPhone
|
||||||
|
Address *UserInfoAddress `json:"address,omitempty"`
|
||||||
|
Claims map[string]any `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAccessTokenHash implements the IDTokenClaims interface
|
||||||
|
func (t *IDTokenClaims) GetAccessTokenHash() string {
|
||||||
|
return t.AccessTokenHash
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *IDTokenClaims) SetUserInfo(i *UserInfo) {
|
||||||
|
t.Subject = i.Subject
|
||||||
|
t.UserInfoProfile = i.UserInfoProfile
|
||||||
|
t.UserInfoEmail = i.UserInfoEmail
|
||||||
|
t.UserInfoPhone = i.UserInfoPhone
|
||||||
|
t.Address = i.Address
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewIDTokenClaims(issuer, subject string, audience []string, expiration, authTime time.Time, nonce string, acr string, amr []string, clientID string, skew time.Duration) *IDTokenClaims {
|
||||||
|
audience = AppendClientIDToAudience(clientID, audience)
|
||||||
|
return &IDTokenClaims{
|
||||||
|
TokenClaims: TokenClaims{
|
||||||
|
Issuer: issuer,
|
||||||
|
Subject: subject,
|
||||||
|
Audience: audience,
|
||||||
|
Expiration: FromTime(expiration),
|
||||||
|
IssuedAt: FromTime(time.Now().Add(-skew)),
|
||||||
|
AuthTime: FromTime(authTime.Add(-skew)),
|
||||||
Nonce: nonce,
|
Nonce: nonce,
|
||||||
AuthenticationContextClassReference: acr,
|
AuthenticationContextClassReference: acr,
|
||||||
AuthenticationMethodsReferences: amr,
|
AuthenticationMethodsReferences: amr,
|
||||||
AuthorizedParty: clientID,
|
AuthorizedParty: clientID,
|
||||||
UserInfo: &userinfo{Subject: subject},
|
ClientID: clientID,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type idTokenClaims struct {
|
type itcAlias IDTokenClaims
|
||||||
Issuer string `json:"iss,omitempty"`
|
|
||||||
Audience Audience `json:"aud,omitempty"`
|
|
||||||
Expiration Time `json:"exp,omitempty"`
|
|
||||||
NotBefore Time `json:"nbf,omitempty"`
|
|
||||||
IssuedAt Time `json:"iat,omitempty"`
|
|
||||||
JWTID string `json:"jti,omitempty"`
|
|
||||||
AuthorizedParty string `json:"azp,omitempty"`
|
|
||||||
Nonce string `json:"nonce,omitempty"`
|
|
||||||
AuthTime Time `json:"auth_time,omitempty"`
|
|
||||||
AccessTokenHash string `json:"at_hash,omitempty"`
|
|
||||||
CodeHash string `json:"c_hash,omitempty"`
|
|
||||||
AuthenticationContextClassReference string `json:"acr,omitempty"`
|
|
||||||
AuthenticationMethodsReferences []string `json:"amr,omitempty"`
|
|
||||||
ClientID string `json:"client_id,omitempty"`
|
|
||||||
UserInfo `json:"-"`
|
|
||||||
|
|
||||||
signatureAlg jose.SignatureAlgorithm
|
func (i *IDTokenClaims) MarshalJSON() ([]byte, error) {
|
||||||
|
return mergeAndMarshalClaims((*itcAlias)(i), i.Claims)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetIssuer implements the Claims interface
|
func (i *IDTokenClaims) UnmarshalJSON(data []byte) error {
|
||||||
func (t *idTokenClaims) GetIssuer() string {
|
return unmarshalJSONMulti(data, (*itcAlias)(i), &i.Claims)
|
||||||
return t.Issuer
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAudience implements the Claims interface
|
|
||||||
func (t *idTokenClaims) GetAudience() []string {
|
|
||||||
return t.Audience
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetExpiration implements the Claims interface
|
|
||||||
func (t *idTokenClaims) GetExpiration() time.Time {
|
|
||||||
return time.Time(t.Expiration)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetIssuedAt implements the Claims interface
|
|
||||||
func (t *idTokenClaims) GetIssuedAt() time.Time {
|
|
||||||
return time.Time(t.IssuedAt)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetNonce implements the Claims interface
|
|
||||||
func (t *idTokenClaims) GetNonce() string {
|
|
||||||
return t.Nonce
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAuthenticationContextClassReference implements the Claims interface
|
|
||||||
func (t *idTokenClaims) GetAuthenticationContextClassReference() string {
|
|
||||||
return t.AuthenticationContextClassReference
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAuthTime implements the Claims interface
|
|
||||||
func (t *idTokenClaims) GetAuthTime() time.Time {
|
|
||||||
return time.Time(t.AuthTime)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAuthorizedParty implements the Claims interface
|
|
||||||
func (t *idTokenClaims) GetAuthorizedParty() string {
|
|
||||||
return t.AuthorizedParty
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetSignatureAlgorithm implements the Claims interface
|
|
||||||
func (t *idTokenClaims) SetSignatureAlgorithm(alg jose.SignatureAlgorithm) {
|
|
||||||
t.signatureAlg = alg
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetNotBefore implements the IDTokenClaims interface
|
|
||||||
func (t *idTokenClaims) GetNotBefore() time.Time {
|
|
||||||
return time.Time(t.NotBefore)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetJWTID implements the IDTokenClaims interface
|
|
||||||
func (t *idTokenClaims) GetJWTID() string {
|
|
||||||
return t.JWTID
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAccessTokenHash implements the IDTokenClaims interface
|
|
||||||
func (t *idTokenClaims) GetAccessTokenHash() string {
|
|
||||||
return t.AccessTokenHash
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetCodeHash implements the IDTokenClaims interface
|
|
||||||
func (t *idTokenClaims) GetCodeHash() string {
|
|
||||||
return t.CodeHash
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAuthenticationMethodsReferences implements the IDTokenClaims interface
|
|
||||||
func (t *idTokenClaims) GetAuthenticationMethodsReferences() []string {
|
|
||||||
return t.AuthenticationMethodsReferences
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetClientID implements the IDTokenClaims interface
|
|
||||||
func (t *idTokenClaims) GetClientID() string {
|
|
||||||
return t.ClientID
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetSignatureAlgorithm implements the IDTokenClaims interface
|
|
||||||
func (t *idTokenClaims) GetSignatureAlgorithm() jose.SignatureAlgorithm {
|
|
||||||
return t.signatureAlg
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetAccessTokenHash implements the IDTokenClaims interface
|
|
||||||
func (t *idTokenClaims) SetAccessTokenHash(hash string) {
|
|
||||||
t.AccessTokenHash = hash
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetUserinfo implements the IDTokenClaims interface
|
|
||||||
func (t *idTokenClaims) SetUserinfo(info UserInfo) {
|
|
||||||
t.UserInfo = info
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetCodeHash implements the IDTokenClaims interface
|
|
||||||
func (t *idTokenClaims) SetCodeHash(hash string) {
|
|
||||||
t.CodeHash = hash
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *idTokenClaims) MarshalJSON() ([]byte, error) {
|
|
||||||
type Alias idTokenClaims
|
|
||||||
a := &struct {
|
|
||||||
*Alias
|
|
||||||
Expiration int64 `json:"exp,omitempty"`
|
|
||||||
IssuedAt int64 `json:"iat,omitempty"`
|
|
||||||
NotBefore int64 `json:"nbf,omitempty"`
|
|
||||||
AuthTime int64 `json:"auth_time,omitempty"`
|
|
||||||
}{
|
|
||||||
Alias: (*Alias)(t),
|
|
||||||
}
|
|
||||||
if !time.Time(t.Expiration).IsZero() {
|
|
||||||
a.Expiration = time.Time(t.Expiration).Unix()
|
|
||||||
}
|
|
||||||
if !time.Time(t.IssuedAt).IsZero() {
|
|
||||||
a.IssuedAt = time.Time(t.IssuedAt).Unix()
|
|
||||||
}
|
|
||||||
if !time.Time(t.NotBefore).IsZero() {
|
|
||||||
a.NotBefore = time.Time(t.NotBefore).Unix()
|
|
||||||
}
|
|
||||||
if !time.Time(t.AuthTime).IsZero() {
|
|
||||||
a.AuthTime = time.Time(t.AuthTime).Unix()
|
|
||||||
}
|
|
||||||
b, err := json.Marshal(a)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if t.UserInfo == nil {
|
|
||||||
return b, nil
|
|
||||||
}
|
|
||||||
info, err := json.Marshal(t.UserInfo)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return http.ConcatenateJSON(b, info)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *idTokenClaims) UnmarshalJSON(data []byte) error {
|
|
||||||
type Alias idTokenClaims
|
|
||||||
if err := json.Unmarshal(data, (*Alias)(t)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
userinfo := new(userinfo)
|
|
||||||
if err := json.Unmarshal(data, userinfo); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
t.UserInfo = userinfo
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type AccessTokenResponse struct {
|
type AccessTokenResponse struct {
|
||||||
|
@ -399,19 +197,7 @@ type AccessTokenResponse struct {
|
||||||
State string `json:"state,omitempty" schema:"state,omitempty"`
|
State string `json:"state,omitempty" schema:"state,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type JWTProfileAssertionClaims interface {
|
type JWTProfileAssertionClaims struct {
|
||||||
GetKeyID() string
|
|
||||||
GetPrivateKey() []byte
|
|
||||||
GetIssuer() string
|
|
||||||
GetSubject() string
|
|
||||||
GetAudience() []string
|
|
||||||
GetExpiration() time.Time
|
|
||||||
GetIssuedAt() time.Time
|
|
||||||
SetCustomClaim(key string, value interface{})
|
|
||||||
GetCustomClaim(key string) interface{}
|
|
||||||
}
|
|
||||||
|
|
||||||
type jwtProfileAssertion struct {
|
|
||||||
PrivateKeyID string `json:"-"`
|
PrivateKeyID string `json:"-"`
|
||||||
PrivateKey []byte `json:"-"`
|
PrivateKey []byte `json:"-"`
|
||||||
Issuer string `json:"iss"`
|
Issuer string `json:"iss"`
|
||||||
|
@ -420,91 +206,21 @@ type jwtProfileAssertion struct {
|
||||||
Expiration Time `json:"exp"`
|
Expiration Time `json:"exp"`
|
||||||
IssuedAt Time `json:"iat"`
|
IssuedAt Time `json:"iat"`
|
||||||
|
|
||||||
customClaims map[string]interface{}
|
Claims map[string]interface{} `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j *jwtProfileAssertion) MarshalJSON() ([]byte, error) {
|
type jpaAlias JWTProfileAssertionClaims
|
||||||
type Alias jwtProfileAssertion
|
|
||||||
a := (*Alias)(j)
|
|
||||||
|
|
||||||
b, err := json.Marshal(a)
|
func (j *JWTProfileAssertionClaims) MarshalJSON() ([]byte, error) {
|
||||||
if err != nil {
|
return mergeAndMarshalClaims((*jpaAlias)(j), j.Claims)
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(j.customClaims) == 0 {
|
|
||||||
return b, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
err = json.Unmarshal(b, &j.customClaims)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("jws: invalid map of custom claims %v", j.customClaims)
|
|
||||||
}
|
|
||||||
|
|
||||||
return json.Marshal(j.customClaims)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j *jwtProfileAssertion) UnmarshalJSON(data []byte) error {
|
func (j *JWTProfileAssertionClaims) UnmarshalJSON(data []byte) error {
|
||||||
type Alias jwtProfileAssertion
|
return unmarshalJSONMulti(data, (*jpaAlias)(j), &j.Claims)
|
||||||
a := (*Alias)(j)
|
|
||||||
|
|
||||||
err := json.Unmarshal(data, a)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = json.Unmarshal(data, &j.customClaims)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j *jwtProfileAssertion) GetKeyID() string {
|
func NewJWTProfileAssertionFromKeyJSON(filename string, audience []string, opts ...AssertionOption) (*JWTProfileAssertionClaims, error) {
|
||||||
return j.PrivateKeyID
|
data, err := os.ReadFile(filename)
|
||||||
}
|
|
||||||
|
|
||||||
func (j *jwtProfileAssertion) GetPrivateKey() []byte {
|
|
||||||
return j.PrivateKey
|
|
||||||
}
|
|
||||||
|
|
||||||
func (j *jwtProfileAssertion) SetCustomClaim(key string, value interface{}) {
|
|
||||||
if j.customClaims == nil {
|
|
||||||
j.customClaims = make(map[string]interface{})
|
|
||||||
}
|
|
||||||
j.customClaims[key] = value
|
|
||||||
}
|
|
||||||
|
|
||||||
func (j *jwtProfileAssertion) GetCustomClaim(key string) interface{} {
|
|
||||||
if j.customClaims == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return j.customClaims[key]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (j *jwtProfileAssertion) GetIssuer() string {
|
|
||||||
return j.Issuer
|
|
||||||
}
|
|
||||||
|
|
||||||
func (j *jwtProfileAssertion) GetSubject() string {
|
|
||||||
return j.Subject
|
|
||||||
}
|
|
||||||
|
|
||||||
func (j *jwtProfileAssertion) GetAudience() []string {
|
|
||||||
return j.Audience
|
|
||||||
}
|
|
||||||
|
|
||||||
func (j *jwtProfileAssertion) GetExpiration() time.Time {
|
|
||||||
return time.Time(j.Expiration)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (j *jwtProfileAssertion) GetIssuedAt() time.Time {
|
|
||||||
return time.Time(j.IssuedAt)
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewJWTProfileAssertionFromKeyJSON(filename string, audience []string, opts ...AssertionOption) (JWTProfileAssertionClaims, error) {
|
|
||||||
data, err := ioutil.ReadFile(filename)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -524,19 +240,19 @@ func NewJWTProfileAssertionStringFromFileData(data []byte, audience []string, op
|
||||||
return GenerateJWTProfileToken(NewJWTProfileAssertion(keyData.UserID, keyData.KeyID, audience, []byte(keyData.Key), opts...))
|
return GenerateJWTProfileToken(NewJWTProfileAssertion(keyData.UserID, keyData.KeyID, audience, []byte(keyData.Key), opts...))
|
||||||
}
|
}
|
||||||
|
|
||||||
func JWTProfileDelegatedSubject(sub string) func(*jwtProfileAssertion) {
|
func JWTProfileDelegatedSubject(sub string) func(*JWTProfileAssertionClaims) {
|
||||||
return func(j *jwtProfileAssertion) {
|
return func(j *JWTProfileAssertionClaims) {
|
||||||
j.Subject = sub
|
j.Subject = sub
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func JWTProfileCustomClaim(key string, value interface{}) func(*jwtProfileAssertion) {
|
func JWTProfileCustomClaim(key string, value interface{}) func(*JWTProfileAssertionClaims) {
|
||||||
return func(j *jwtProfileAssertion) {
|
return func(j *JWTProfileAssertionClaims) {
|
||||||
j.customClaims[key] = value
|
j.Claims[key] = value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewJWTProfileAssertionFromFileData(data []byte, audience []string, opts ...AssertionOption) (JWTProfileAssertionClaims, error) {
|
func NewJWTProfileAssertionFromFileData(data []byte, audience []string, opts ...AssertionOption) (*JWTProfileAssertionClaims, error) {
|
||||||
keyData := new(struct {
|
keyData := new(struct {
|
||||||
KeyID string `json:"keyId"`
|
KeyID string `json:"keyId"`
|
||||||
Key string `json:"key"`
|
Key string `json:"key"`
|
||||||
|
@ -549,18 +265,18 @@ func NewJWTProfileAssertionFromFileData(data []byte, audience []string, opts ...
|
||||||
return NewJWTProfileAssertion(keyData.UserID, keyData.KeyID, audience, []byte(keyData.Key), opts...), nil
|
return NewJWTProfileAssertion(keyData.UserID, keyData.KeyID, audience, []byte(keyData.Key), opts...), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type AssertionOption func(*jwtProfileAssertion)
|
type AssertionOption func(*JWTProfileAssertionClaims)
|
||||||
|
|
||||||
func NewJWTProfileAssertion(userID, keyID string, audience []string, key []byte, opts ...AssertionOption) JWTProfileAssertionClaims {
|
func NewJWTProfileAssertion(userID, keyID string, audience []string, key []byte, opts ...AssertionOption) *JWTProfileAssertionClaims {
|
||||||
j := &jwtProfileAssertion{
|
j := &JWTProfileAssertionClaims{
|
||||||
PrivateKey: key,
|
PrivateKey: key,
|
||||||
PrivateKeyID: keyID,
|
PrivateKeyID: keyID,
|
||||||
Issuer: userID,
|
Issuer: userID,
|
||||||
Subject: userID,
|
Subject: userID,
|
||||||
IssuedAt: Time(time.Now().UTC()),
|
IssuedAt: FromTime(time.Now().UTC()),
|
||||||
Expiration: Time(time.Now().Add(1 * time.Hour).UTC()),
|
Expiration: FromTime(time.Now().Add(1 * time.Hour).UTC()),
|
||||||
Audience: audience,
|
Audience: audience,
|
||||||
customClaims: make(map[string]interface{}),
|
Claims: make(map[string]interface{}),
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, opt := range opts {
|
for _, opt := range opts {
|
||||||
|
@ -588,14 +304,14 @@ func AppendClientIDToAudience(clientID string, audience []string) []string {
|
||||||
return append(audience, clientID)
|
return append(audience, clientID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func GenerateJWTProfileToken(assertion JWTProfileAssertionClaims) (string, error) {
|
func GenerateJWTProfileToken(assertion *JWTProfileAssertionClaims) (string, error) {
|
||||||
privateKey, err := crypto.BytesToPrivateKey(assertion.GetPrivateKey())
|
privateKey, err := crypto.BytesToPrivateKey(assertion.PrivateKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
key := jose.SigningKey{
|
key := jose.SigningKey{
|
||||||
Algorithm: jose.RS256,
|
Algorithm: jose.RS256,
|
||||||
Key: &jose.JSONWebKey{Key: privateKey, KeyID: assertion.GetKeyID()},
|
Key: &jose.JSONWebKey{Key: privateKey, KeyID: assertion.PrivateKeyID},
|
||||||
}
|
}
|
||||||
signer, err := jose.NewSigner(key, &jose.SignerOptions{})
|
signer, err := jose.NewSigner(key, &jose.SignerOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -612,3 +328,12 @@ func GenerateJWTProfileToken(assertion JWTProfileAssertionClaims) (string, error
|
||||||
}
|
}
|
||||||
return signedAssertion.CompactSerialize()
|
return signedAssertion.CompactSerialize()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TokenExchangeResponse struct {
|
||||||
|
AccessToken string `json:"access_token"` // Can be access token or ID token
|
||||||
|
IssuedTokenType TokenType `json:"issued_token_type"`
|
||||||
|
TokenType string `json:"token_type"`
|
||||||
|
ExpiresIn uint64 `json:"expires_in,omitempty"`
|
||||||
|
Scopes SpaceDelimitedArray `json:"scope,omitempty"`
|
||||||
|
RefreshToken string `json:"refresh_token,omitempty"`
|
||||||
|
}
|
||||||
|
|
|
@ -27,6 +27,9 @@ const (
|
||||||
// GrantTypeImplicit defines the grant type `implicit` used for implicit flows that skip the generation and exchange of an Authorization Code
|
// GrantTypeImplicit defines the grant type `implicit` used for implicit flows that skip the generation and exchange of an Authorization Code
|
||||||
GrantTypeImplicit GrantType = "implicit"
|
GrantTypeImplicit GrantType = "implicit"
|
||||||
|
|
||||||
|
// GrantTypeDeviceCode
|
||||||
|
GrantTypeDeviceCode GrantType = "urn:ietf:params:oauth:grant-type:device_code"
|
||||||
|
|
||||||
// ClientAssertionTypeJWTAssertion defines the client_assertion_type `urn:ietf:params:oauth:client-assertion-type:jwt-bearer`
|
// ClientAssertionTypeJWTAssertion defines the client_assertion_type `urn:ietf:params:oauth:client-assertion-type:jwt-bearer`
|
||||||
// used for the OAuth JWT Profile Client Authentication
|
// used for the OAuth JWT Profile Client Authentication
|
||||||
ClientAssertionTypeJWTAssertion = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
|
ClientAssertionTypeJWTAssertion = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
|
||||||
|
@ -35,11 +38,34 @@ const (
|
||||||
var AllGrantTypes = []GrantType{
|
var AllGrantTypes = []GrantType{
|
||||||
GrantTypeCode, GrantTypeRefreshToken, GrantTypeClientCredentials,
|
GrantTypeCode, GrantTypeRefreshToken, GrantTypeClientCredentials,
|
||||||
GrantTypeBearer, GrantTypeTokenExchange, GrantTypeImplicit,
|
GrantTypeBearer, GrantTypeTokenExchange, GrantTypeImplicit,
|
||||||
ClientAssertionTypeJWTAssertion,
|
GrantTypeDeviceCode, ClientAssertionTypeJWTAssertion,
|
||||||
}
|
}
|
||||||
|
|
||||||
type GrantType string
|
type GrantType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
AccessTokenType TokenType = "urn:ietf:params:oauth:token-type:access_token"
|
||||||
|
RefreshTokenType TokenType = "urn:ietf:params:oauth:token-type:refresh_token"
|
||||||
|
IDTokenType TokenType = "urn:ietf:params:oauth:token-type:id_token"
|
||||||
|
JWTTokenType TokenType = "urn:ietf:params:oauth:token-type:jwt"
|
||||||
|
)
|
||||||
|
|
||||||
|
var AllTokenTypes = []TokenType{
|
||||||
|
AccessTokenType, RefreshTokenType, IDTokenType, JWTTokenType,
|
||||||
|
}
|
||||||
|
|
||||||
|
type TokenType string
|
||||||
|
|
||||||
|
func (t TokenType) IsSupported() bool {
|
||||||
|
for _, tt := range AllTokenTypes {
|
||||||
|
if t == tt {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
type TokenRequest interface {
|
type TokenRequest interface {
|
||||||
// GrantType GrantType `schema:"grant_type"`
|
// GrantType GrantType `schema:"grant_type"`
|
||||||
GrantType() GrantType
|
GrantType() GrantType
|
||||||
|
@ -161,12 +187,12 @@ func (j *JWTTokenRequest) GetAudience() []string {
|
||||||
|
|
||||||
// GetExpiration implements the Claims interface
|
// GetExpiration implements the Claims interface
|
||||||
func (j *JWTTokenRequest) GetExpiration() time.Time {
|
func (j *JWTTokenRequest) GetExpiration() time.Time {
|
||||||
return time.Time(j.ExpiresAt)
|
return j.ExpiresAt.AsTime()
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetIssuedAt implements the Claims interface
|
// GetIssuedAt implements the Claims interface
|
||||||
func (j *JWTTokenRequest) GetIssuedAt() time.Time {
|
func (j *JWTTokenRequest) GetIssuedAt() time.Time {
|
||||||
return time.Time(j.IssuedAt)
|
return j.ExpiresAt.AsTime()
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetNonce implements the Claims interface
|
// GetNonce implements the Claims interface
|
||||||
|
@ -203,14 +229,15 @@ func (j *JWTTokenRequest) GetScopes() []string {
|
||||||
}
|
}
|
||||||
|
|
||||||
type TokenExchangeRequest struct {
|
type TokenExchangeRequest struct {
|
||||||
subjectToken string `schema:"subject_token"`
|
GrantType GrantType `schema:"grant_type"`
|
||||||
subjectTokenType string `schema:"subject_token_type"`
|
SubjectToken string `schema:"subject_token"`
|
||||||
actorToken string `schema:"actor_token"`
|
SubjectTokenType TokenType `schema:"subject_token_type"`
|
||||||
actorTokenType string `schema:"actor_token_type"`
|
ActorToken string `schema:"actor_token"`
|
||||||
resource []string `schema:"resource"`
|
ActorTokenType TokenType `schema:"actor_token_type"`
|
||||||
audience Audience `schema:"audience"`
|
Resource []string `schema:"resource"`
|
||||||
Scope SpaceDelimitedArray `schema:"scope"`
|
Audience Audience `schema:"audience"`
|
||||||
requestedTokenType string `schema:"requested_token_type"`
|
Scopes SpaceDelimitedArray `schema:"scope"`
|
||||||
|
RequestedTokenType TokenType `schema:"requested_token_type"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ClientCredentialsRequest struct {
|
type ClientCredentialsRequest struct {
|
||||||
|
@ -218,4 +245,6 @@ type ClientCredentialsRequest struct {
|
||||||
Scope SpaceDelimitedArray `schema:"scope"`
|
Scope SpaceDelimitedArray `schema:"scope"`
|
||||||
ClientID string `schema:"client_id"`
|
ClientID string `schema:"client_id"`
|
||||||
ClientSecret string `schema:"client_secret"`
|
ClientSecret string `schema:"client_secret"`
|
||||||
|
ClientAssertion string `schema:"client_assertion"`
|
||||||
|
ClientAssertionType string `schema:"client_assertion_type"`
|
||||||
}
|
}
|
||||||
|
|
227
pkg/oidc/token_test.go
Normal file
227
pkg/oidc/token_test.go
Normal file
|
@ -0,0 +1,227 @@
|
||||||
|
package oidc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"golang.org/x/text/language"
|
||||||
|
"gopkg.in/square/go-jose.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
tokenClaimsData = TokenClaims{
|
||||||
|
Issuer: "zitadel",
|
||||||
|
Subject: "hello@me.com",
|
||||||
|
Audience: Audience{"foo", "bar"},
|
||||||
|
Expiration: 12345,
|
||||||
|
IssuedAt: 12000,
|
||||||
|
JWTID: "900",
|
||||||
|
AuthorizedParty: "just@me.com",
|
||||||
|
Nonce: "6969",
|
||||||
|
AuthTime: 12000,
|
||||||
|
NotBefore: 12000,
|
||||||
|
AuthenticationContextClassReference: "something",
|
||||||
|
AuthenticationMethodsReferences: []string{"some", "methods"},
|
||||||
|
ClientID: "777",
|
||||||
|
SignatureAlg: jose.ES256,
|
||||||
|
}
|
||||||
|
accessTokenData = &AccessTokenClaims{
|
||||||
|
TokenClaims: tokenClaimsData,
|
||||||
|
Scopes: []string{"email", "phone"},
|
||||||
|
Claims: map[string]interface{}{
|
||||||
|
"foo": "bar",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
idTokenData = &IDTokenClaims{
|
||||||
|
TokenClaims: tokenClaimsData,
|
||||||
|
NotBefore: 12000,
|
||||||
|
AccessTokenHash: "acthashhash",
|
||||||
|
CodeHash: "hashhash",
|
||||||
|
SessionID: "666",
|
||||||
|
UserInfoProfile: userInfoData.UserInfoProfile,
|
||||||
|
UserInfoEmail: userInfoData.UserInfoEmail,
|
||||||
|
UserInfoPhone: userInfoData.UserInfoPhone,
|
||||||
|
Address: userInfoData.Address,
|
||||||
|
Claims: map[string]interface{}{
|
||||||
|
"foo": "bar",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
introspectionResponseData = &IntrospectionResponse{
|
||||||
|
Active: true,
|
||||||
|
Scope: SpaceDelimitedArray{"email", "phone"},
|
||||||
|
ClientID: "777",
|
||||||
|
TokenType: "idtoken",
|
||||||
|
Expiration: 12345,
|
||||||
|
IssuedAt: 12000,
|
||||||
|
NotBefore: 12000,
|
||||||
|
Subject: "hello@me.com",
|
||||||
|
Audience: Audience{"foo", "bar"},
|
||||||
|
Issuer: "zitadel",
|
||||||
|
JWTID: "900",
|
||||||
|
Username: "muhlemmer",
|
||||||
|
UserInfoProfile: userInfoData.UserInfoProfile,
|
||||||
|
UserInfoEmail: userInfoData.UserInfoEmail,
|
||||||
|
UserInfoPhone: userInfoData.UserInfoPhone,
|
||||||
|
Address: userInfoData.Address,
|
||||||
|
Claims: map[string]interface{}{
|
||||||
|
"foo": "bar",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
userInfoData = &UserInfo{
|
||||||
|
Subject: "hello@me.com",
|
||||||
|
UserInfoProfile: UserInfoProfile{
|
||||||
|
Name: "Tim Möhlmann",
|
||||||
|
GivenName: "Tim",
|
||||||
|
FamilyName: "Möhlmann",
|
||||||
|
MiddleName: "Danger",
|
||||||
|
Nickname: "muhlemmer",
|
||||||
|
Profile: "https://github.com/muhlemmer",
|
||||||
|
Picture: "https://avatars.githubusercontent.com/u/5411563?v=4",
|
||||||
|
Website: "https://zitadel.com",
|
||||||
|
Gender: "male",
|
||||||
|
Birthdate: "1st of April",
|
||||||
|
Zoneinfo: "Europe/Amsterdam",
|
||||||
|
Locale: NewLocale(language.Dutch),
|
||||||
|
UpdatedAt: 1,
|
||||||
|
PreferredUsername: "muhlemmer",
|
||||||
|
},
|
||||||
|
UserInfoEmail: UserInfoEmail{
|
||||||
|
Email: "tim@zitadel.com",
|
||||||
|
EmailVerified: true,
|
||||||
|
},
|
||||||
|
UserInfoPhone: UserInfoPhone{
|
||||||
|
PhoneNumber: "+1234567890",
|
||||||
|
PhoneNumberVerified: true,
|
||||||
|
},
|
||||||
|
Address: &UserInfoAddress{
|
||||||
|
Formatted: "Sesame street 666\n666-666, Smallvile\nMoon",
|
||||||
|
StreetAddress: "Sesame street 666",
|
||||||
|
Locality: "Smallvile",
|
||||||
|
Region: "Outer space",
|
||||||
|
PostalCode: "666-666",
|
||||||
|
Country: "Moon",
|
||||||
|
},
|
||||||
|
Claims: map[string]interface{}{
|
||||||
|
"foo": "bar",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
jwtProfileAssertionData = &JWTProfileAssertionClaims{
|
||||||
|
PrivateKeyID: "8888",
|
||||||
|
PrivateKey: []byte("qwerty"),
|
||||||
|
Issuer: "zitadel",
|
||||||
|
Subject: "hello@me.com",
|
||||||
|
Audience: Audience{"foo", "bar"},
|
||||||
|
Expiration: 12345,
|
||||||
|
IssuedAt: 12000,
|
||||||
|
Claims: map[string]interface{}{
|
||||||
|
"foo": "bar",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTokenClaims(t *testing.T) {
|
||||||
|
claims := tokenClaimsData
|
||||||
|
|
||||||
|
assert.Equal(t, claims.Issuer, tokenClaimsData.GetIssuer())
|
||||||
|
assert.Equal(t, claims.Subject, tokenClaimsData.GetSubject())
|
||||||
|
assert.Equal(t, []string(claims.Audience), tokenClaimsData.GetAudience())
|
||||||
|
assert.Equal(t, claims.Expiration.AsTime(), tokenClaimsData.GetExpiration())
|
||||||
|
assert.Equal(t, claims.IssuedAt.AsTime(), tokenClaimsData.GetIssuedAt())
|
||||||
|
assert.Equal(t, claims.Nonce, tokenClaimsData.GetNonce())
|
||||||
|
assert.Equal(t, claims.AuthTime.AsTime(), tokenClaimsData.GetAuthTime())
|
||||||
|
assert.Equal(t, claims.AuthorizedParty, tokenClaimsData.GetAuthorizedParty())
|
||||||
|
assert.Equal(t, claims.SignatureAlg, tokenClaimsData.GetSignatureAlgorithm())
|
||||||
|
assert.Equal(t, claims.AuthenticationContextClassReference, tokenClaimsData.GetAuthenticationContextClassReference())
|
||||||
|
|
||||||
|
claims.SetSignatureAlgorithm(jose.ES384)
|
||||||
|
assert.Equal(t, jose.ES384, claims.SignatureAlg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewAccessTokenClaims(t *testing.T) {
|
||||||
|
want := &AccessTokenClaims{
|
||||||
|
TokenClaims: TokenClaims{
|
||||||
|
Issuer: "zitadel",
|
||||||
|
Subject: "hello@me.com",
|
||||||
|
Audience: Audience{"foo"},
|
||||||
|
Expiration: 12345,
|
||||||
|
JWTID: "900",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
got := NewAccessTokenClaims(
|
||||||
|
want.Issuer, want.Subject, nil,
|
||||||
|
want.Expiration.AsTime(), want.JWTID, "foo", time.Second,
|
||||||
|
)
|
||||||
|
|
||||||
|
// test if the dynamic timestamps are around now,
|
||||||
|
// allowing for a delta of 1, just in case we flip on
|
||||||
|
// either side of a second boundry.
|
||||||
|
nowMinusSkew := NowTime() - 1
|
||||||
|
assert.InDelta(t, int64(nowMinusSkew), int64(got.IssuedAt), 1)
|
||||||
|
assert.InDelta(t, int64(nowMinusSkew), int64(got.NotBefore), 1)
|
||||||
|
|
||||||
|
// Make equal not fail on dynamic timestamp
|
||||||
|
got.IssuedAt = 0
|
||||||
|
got.NotBefore = 0
|
||||||
|
|
||||||
|
assert.Equal(t, want, got)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIDTokenClaims_GetAccessTokenHash(t *testing.T) {
|
||||||
|
assert.Equal(t, idTokenData.AccessTokenHash, idTokenData.GetAccessTokenHash())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIDTokenClaims_SetUserInfo(t *testing.T) {
|
||||||
|
want := IDTokenClaims{
|
||||||
|
TokenClaims: TokenClaims{
|
||||||
|
Subject: userInfoData.Subject,
|
||||||
|
},
|
||||||
|
UserInfoProfile: userInfoData.UserInfoProfile,
|
||||||
|
UserInfoEmail: userInfoData.UserInfoEmail,
|
||||||
|
UserInfoPhone: userInfoData.UserInfoPhone,
|
||||||
|
Address: userInfoData.Address,
|
||||||
|
}
|
||||||
|
|
||||||
|
var got IDTokenClaims
|
||||||
|
got.SetUserInfo(userInfoData)
|
||||||
|
|
||||||
|
assert.Equal(t, want, got)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewIDTokenClaims(t *testing.T) {
|
||||||
|
want := &IDTokenClaims{
|
||||||
|
TokenClaims: TokenClaims{
|
||||||
|
Issuer: "zitadel",
|
||||||
|
Subject: "hello@me.com",
|
||||||
|
Audience: Audience{"foo", "just@me.com"},
|
||||||
|
Expiration: 12345,
|
||||||
|
AuthTime: 12000,
|
||||||
|
Nonce: "6969",
|
||||||
|
AuthenticationContextClassReference: "something",
|
||||||
|
AuthenticationMethodsReferences: []string{"some", "methods"},
|
||||||
|
AuthorizedParty: "just@me.com",
|
||||||
|
ClientID: "just@me.com",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
got := NewIDTokenClaims(
|
||||||
|
want.Issuer, want.Subject, want.Audience,
|
||||||
|
want.Expiration.AsTime(),
|
||||||
|
want.AuthTime.AsTime().Add(time.Second),
|
||||||
|
want.Nonce, want.AuthenticationContextClassReference,
|
||||||
|
want.AuthenticationMethodsReferences, want.AuthorizedParty,
|
||||||
|
time.Second,
|
||||||
|
)
|
||||||
|
|
||||||
|
// test if the dynamic timestamp is around now,
|
||||||
|
// allowing for a delta of 1, just in case we flip on
|
||||||
|
// either side of a second boundry.
|
||||||
|
nowMinusSkew := NowTime() - 1
|
||||||
|
assert.InDelta(t, int64(nowMinusSkew), int64(got.IssuedAt), 1)
|
||||||
|
|
||||||
|
// Make equal not fail on dynamic timestamp
|
||||||
|
got.IssuedAt = 0
|
||||||
|
|
||||||
|
assert.Equal(t, want, got)
|
||||||
|
}
|
|
@ -4,9 +4,11 @@ import (
|
||||||
"database/sql/driver"
|
"database/sql/driver"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/gorilla/schema"
|
||||||
"golang.org/x/text/language"
|
"golang.org/x/text/language"
|
||||||
"gopkg.in/square/go-jose.v2"
|
"gopkg.in/square/go-jose.v2"
|
||||||
)
|
)
|
||||||
|
@ -44,6 +46,39 @@ func (d *Display) UnmarshalText(text []byte) error {
|
||||||
|
|
||||||
type Gender string
|
type Gender string
|
||||||
|
|
||||||
|
type Locale struct {
|
||||||
|
tag language.Tag
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLocale(tag language.Tag) *Locale {
|
||||||
|
return &Locale{tag: tag}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Locale) Tag() language.Tag {
|
||||||
|
if l == nil {
|
||||||
|
return language.Und
|
||||||
|
}
|
||||||
|
|
||||||
|
return l.tag
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Locale) String() string {
|
||||||
|
return l.Tag().String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Locale) MarshalJSON() ([]byte, error) {
|
||||||
|
tag := l.Tag()
|
||||||
|
if tag.IsRoot() {
|
||||||
|
return []byte("null"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return json.Marshal(tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Locale) UnmarshalJSON(data []byte) error {
|
||||||
|
return json.Unmarshal(data, &l.tag)
|
||||||
|
}
|
||||||
|
|
||||||
type Locales []language.Tag
|
type Locales []language.Tag
|
||||||
|
|
||||||
func (l *Locales) UnmarshalText(text []byte) error {
|
func (l *Locales) UnmarshalText(text []byte) error {
|
||||||
|
@ -125,19 +160,52 @@ func (s SpaceDelimitedArray) Value() (driver.Value, error) {
|
||||||
return strings.Join(s, " "), nil
|
return strings.Join(s, " "), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type Time time.Time
|
// NewEncoder returns a schema Encoder with
|
||||||
|
// a registered encoder for SpaceDelimitedArray.
|
||||||
func (t *Time) UnmarshalJSON(data []byte) error {
|
func NewEncoder() *schema.Encoder {
|
||||||
var i int64
|
e := schema.NewEncoder()
|
||||||
if err := json.Unmarshal(data, &i); err != nil {
|
e.RegisterEncoder(SpaceDelimitedArray{}, func(value reflect.Value) string {
|
||||||
return err
|
return value.Interface().(SpaceDelimitedArray).Encode()
|
||||||
}
|
})
|
||||||
*t = Time(time.Unix(i, 0).UTC())
|
return e
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Time) MarshalJSON() ([]byte, error) {
|
type Time int64
|
||||||
return json.Marshal(time.Time(*t).UTC().Unix())
|
|
||||||
|
func (ts Time) AsTime() time.Time {
|
||||||
|
return time.Unix(int64(ts), 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func FromTime(tt time.Time) Time {
|
||||||
|
return Time(tt.Unix())
|
||||||
|
}
|
||||||
|
|
||||||
|
func NowTime() Time {
|
||||||
|
return FromTime(time.Now())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ts *Time) UnmarshalJSON(data []byte) error {
|
||||||
|
var v any
|
||||||
|
if err := json.Unmarshal(data, &v); err != nil {
|
||||||
|
return fmt.Errorf("oidc.Time: %w", err)
|
||||||
|
}
|
||||||
|
switch x := v.(type) {
|
||||||
|
case float64:
|
||||||
|
*ts = Time(x)
|
||||||
|
case string:
|
||||||
|
// Compatibility with Auth0:
|
||||||
|
// https://github.com/zitadel/oidc/issues/292
|
||||||
|
tt, err := time.Parse(time.RFC3339, x)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("oidc.Time: %w", err)
|
||||||
|
}
|
||||||
|
*ts = FromTime(tt)
|
||||||
|
case nil:
|
||||||
|
*ts = 0
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("oidc.Time: unable to parse type %T with value %v", x, x)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type RequestObject struct {
|
type RequestObject struct {
|
||||||
|
@ -150,5 +218,4 @@ func (r *RequestObject) GetIssuer() string {
|
||||||
return r.Issuer
|
return r.Issuer
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RequestObject) SetSignatureAlgorithm(algorithm jose.SignatureAlgorithm) {
|
func (*RequestObject) SetSignatureAlgorithm(algorithm jose.SignatureAlgorithm) {}
|
||||||
}
|
|
||||||
|
|
|
@ -3,11 +3,14 @@ package oidc
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/gorilla/schema"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
"golang.org/x/text/language"
|
"golang.org/x/text/language"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -109,6 +112,117 @@ func TestDisplay_UnmarshalText(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLocale_Tag(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
l *Locale
|
||||||
|
want language.Tag
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "nil",
|
||||||
|
l: nil,
|
||||||
|
want: language.Und,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Und",
|
||||||
|
l: NewLocale(language.Und),
|
||||||
|
want: language.Und,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "language",
|
||||||
|
l: NewLocale(language.Afrikaans),
|
||||||
|
want: language.Afrikaans,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
assert.Equal(t, tt.want, tt.l.Tag())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLocale_String(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
l *Locale
|
||||||
|
want language.Tag
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "nil",
|
||||||
|
l: nil,
|
||||||
|
want: language.Und,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Und",
|
||||||
|
l: NewLocale(language.Und),
|
||||||
|
want: language.Und,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "language",
|
||||||
|
l: NewLocale(language.Afrikaans),
|
||||||
|
want: language.Afrikaans,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
assert.Equal(t, tt.want.String(), tt.l.String())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLocale_MarshalJSON(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
l *Locale
|
||||||
|
want string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "nil",
|
||||||
|
l: nil,
|
||||||
|
want: "null",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "und",
|
||||||
|
l: NewLocale(language.Und),
|
||||||
|
want: "null",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "language",
|
||||||
|
l: NewLocale(language.Afrikaans),
|
||||||
|
want: `"af"`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got, err := json.Marshal(tt.l)
|
||||||
|
if tt.wantErr {
|
||||||
|
require.Error(t, err)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
assert.Equal(t, tt.want, string(got))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLocale_UnmarshalJSON(t *testing.T) {
|
||||||
|
type a struct {
|
||||||
|
Locale *Locale `json:"locale,omitempty"`
|
||||||
|
}
|
||||||
|
want := a{
|
||||||
|
Locale: NewLocale(language.Afrikaans),
|
||||||
|
}
|
||||||
|
|
||||||
|
const input = `{"locale": "af"}`
|
||||||
|
var got a
|
||||||
|
|
||||||
|
require.NoError(t,
|
||||||
|
json.Unmarshal([]byte(input), &got),
|
||||||
|
)
|
||||||
|
assert.Equal(t, want, got)
|
||||||
|
}
|
||||||
|
|
||||||
func TestLocales_UnmarshalText(t *testing.T) {
|
func TestLocales_UnmarshalText(t *testing.T) {
|
||||||
type args struct {
|
type args struct {
|
||||||
text []byte
|
text []byte
|
||||||
|
@ -335,3 +449,74 @@ func TestSpaceDelimitatedArray_ValuerNil(t *testing.T) {
|
||||||
assert.Equal(t, SpaceDelimitedArray(nil), reversed, "scan nil")
|
assert.Equal(t, SpaceDelimitedArray(nil), reversed, "scan nil")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNewEncoder(t *testing.T) {
|
||||||
|
type request struct {
|
||||||
|
Scopes SpaceDelimitedArray `schema:"scope"`
|
||||||
|
}
|
||||||
|
a := request{
|
||||||
|
Scopes: SpaceDelimitedArray{"foo", "bar"},
|
||||||
|
}
|
||||||
|
|
||||||
|
values := make(url.Values)
|
||||||
|
NewEncoder().Encode(a, values)
|
||||||
|
assert.Equal(t, url.Values{"scope": []string{"foo bar"}}, values)
|
||||||
|
|
||||||
|
var b request
|
||||||
|
schema.NewDecoder().Decode(&b, values)
|
||||||
|
assert.Equal(t, a, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTime_UnmarshalJSON(t *testing.T) {
|
||||||
|
type dst struct {
|
||||||
|
UpdatedAt Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
json string
|
||||||
|
want dst
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "RFC3339", // https://github.com/zitadel/oidc/issues/292
|
||||||
|
json: `{"updated_at": "2021-05-11T21:13:25.566Z"}`,
|
||||||
|
want: dst{UpdatedAt: 1620767605},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "int",
|
||||||
|
json: `{"updated_at":1620767605}`,
|
||||||
|
want: dst{UpdatedAt: 1620767605},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "time parse error",
|
||||||
|
json: `{"updated_at":"foo"}`,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "null",
|
||||||
|
json: `{"updated_at":null}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid type",
|
||||||
|
json: `{"updated_at":["foo","bar"]}`,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
var got dst
|
||||||
|
err := json.Unmarshal([]byte(tt.json), &got)
|
||||||
|
if tt.wantErr {
|
||||||
|
assert.Error(t, err)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
assert.Equal(t, tt.want, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
t.Run("syntax error", func(t *testing.T) {
|
||||||
|
var ts Time
|
||||||
|
err := ts.UnmarshalJSON([]byte{'~'})
|
||||||
|
assert.Error(t, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -1,292 +1,45 @@
|
||||||
package oidc
|
package oidc
|
||||||
|
|
||||||
import (
|
// UserInfo implements OpenID Connect Core 1.0, section 5.1.
|
||||||
"encoding/json"
|
// https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims.
|
||||||
"fmt"
|
type UserInfo struct {
|
||||||
"time"
|
Subject string `json:"sub,omitempty"`
|
||||||
|
|
||||||
"golang.org/x/text/language"
|
|
||||||
)
|
|
||||||
|
|
||||||
type UserInfo interface {
|
|
||||||
GetSubject() string
|
|
||||||
UserInfoProfile
|
UserInfoProfile
|
||||||
UserInfoEmail
|
UserInfoEmail
|
||||||
UserInfoPhone
|
UserInfoPhone
|
||||||
GetAddress() UserInfoAddress
|
Address *UserInfoAddress `json:"address,omitempty"`
|
||||||
GetClaim(key string) interface{}
|
|
||||||
GetClaims() map[string]interface{}
|
Claims map[string]any `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserInfoProfile interface {
|
func (u *UserInfo) AppendClaims(k string, v any) {
|
||||||
GetName() string
|
if u.Claims == nil {
|
||||||
GetGivenName() string
|
u.Claims = make(map[string]any)
|
||||||
GetFamilyName() string
|
}
|
||||||
GetMiddleName() string
|
|
||||||
GetNickname() string
|
u.Claims[k] = v
|
||||||
GetProfile() string
|
|
||||||
GetPicture() string
|
|
||||||
GetWebsite() string
|
|
||||||
GetGender() Gender
|
|
||||||
GetBirthdate() string
|
|
||||||
GetZoneinfo() string
|
|
||||||
GetLocale() language.Tag
|
|
||||||
GetPreferredUsername() string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserInfoEmail interface {
|
// GetAddress is a safe getter that takes
|
||||||
GetEmail() string
|
// care of a possible nil value.
|
||||||
IsEmailVerified() bool
|
func (u *UserInfo) GetAddress() *UserInfoAddress {
|
||||||
}
|
|
||||||
|
|
||||||
type UserInfoPhone interface {
|
|
||||||
GetPhoneNumber() string
|
|
||||||
IsPhoneNumberVerified() bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type UserInfoAddress interface {
|
|
||||||
GetFormatted() string
|
|
||||||
GetStreetAddress() string
|
|
||||||
GetLocality() string
|
|
||||||
GetRegion() string
|
|
||||||
GetPostalCode() string
|
|
||||||
GetCountry() string
|
|
||||||
}
|
|
||||||
|
|
||||||
type UserInfoSetter interface {
|
|
||||||
UserInfo
|
|
||||||
SetSubject(sub string)
|
|
||||||
UserInfoProfileSetter
|
|
||||||
SetEmail(email string, verified bool)
|
|
||||||
SetPhone(phone string, verified bool)
|
|
||||||
SetAddress(address UserInfoAddress)
|
|
||||||
AppendClaims(key string, values interface{})
|
|
||||||
}
|
|
||||||
|
|
||||||
type UserInfoProfileSetter interface {
|
|
||||||
SetName(name string)
|
|
||||||
SetGivenName(name string)
|
|
||||||
SetFamilyName(name string)
|
|
||||||
SetMiddleName(name string)
|
|
||||||
SetNickname(name string)
|
|
||||||
SetUpdatedAt(date time.Time)
|
|
||||||
SetProfile(profile string)
|
|
||||||
SetPicture(profile string)
|
|
||||||
SetWebsite(website string)
|
|
||||||
SetGender(gender Gender)
|
|
||||||
SetBirthdate(birthdate string)
|
|
||||||
SetZoneinfo(zoneInfo string)
|
|
||||||
SetLocale(locale language.Tag)
|
|
||||||
SetPreferredUsername(name string)
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewUserInfo() UserInfoSetter {
|
|
||||||
return &userinfo{}
|
|
||||||
}
|
|
||||||
|
|
||||||
type userinfo struct {
|
|
||||||
Subject string `json:"sub,omitempty"`
|
|
||||||
userInfoProfile
|
|
||||||
userInfoEmail
|
|
||||||
userInfoPhone
|
|
||||||
Address UserInfoAddress `json:"address,omitempty"`
|
|
||||||
|
|
||||||
claims map[string]interface{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *userinfo) GetSubject() string {
|
|
||||||
return u.Subject
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *userinfo) GetName() string {
|
|
||||||
return u.Name
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *userinfo) GetGivenName() string {
|
|
||||||
return u.GivenName
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *userinfo) GetFamilyName() string {
|
|
||||||
return u.FamilyName
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *userinfo) GetMiddleName() string {
|
|
||||||
return u.MiddleName
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *userinfo) GetNickname() string {
|
|
||||||
return u.Nickname
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *userinfo) GetProfile() string {
|
|
||||||
return u.Profile
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *userinfo) GetPicture() string {
|
|
||||||
return u.Picture
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *userinfo) GetWebsite() string {
|
|
||||||
return u.Website
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *userinfo) GetGender() Gender {
|
|
||||||
return u.Gender
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *userinfo) GetBirthdate() string {
|
|
||||||
return u.Birthdate
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *userinfo) GetZoneinfo() string {
|
|
||||||
return u.Zoneinfo
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *userinfo) GetLocale() language.Tag {
|
|
||||||
return u.Locale
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *userinfo) GetPreferredUsername() string {
|
|
||||||
return u.PreferredUsername
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *userinfo) GetEmail() string {
|
|
||||||
return u.Email
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *userinfo) IsEmailVerified() bool {
|
|
||||||
return bool(u.EmailVerified)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *userinfo) GetPhoneNumber() string {
|
|
||||||
return u.PhoneNumber
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *userinfo) IsPhoneNumberVerified() bool {
|
|
||||||
return u.PhoneNumberVerified
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *userinfo) GetAddress() UserInfoAddress {
|
|
||||||
if u.Address == nil {
|
if u.Address == nil {
|
||||||
return &userInfoAddress{}
|
return new(UserInfoAddress)
|
||||||
}
|
}
|
||||||
return u.Address
|
return u.Address
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *userinfo) GetClaim(key string) interface{} {
|
type uiAlias UserInfo
|
||||||
return u.claims[key]
|
|
||||||
|
func (u *UserInfo) MarshalJSON() ([]byte, error) {
|
||||||
|
return mergeAndMarshalClaims((*uiAlias)(u), u.Claims)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *userinfo) GetClaims() map[string]interface{} {
|
func (u *UserInfo) UnmarshalJSON(data []byte) error {
|
||||||
return u.claims
|
return unmarshalJSONMulti(data, (*uiAlias)(u), &u.Claims)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *userinfo) SetSubject(sub string) {
|
type UserInfoProfile struct {
|
||||||
u.Subject = sub
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *userinfo) SetName(name string) {
|
|
||||||
u.Name = name
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *userinfo) SetGivenName(name string) {
|
|
||||||
u.GivenName = name
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *userinfo) SetFamilyName(name string) {
|
|
||||||
u.FamilyName = name
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *userinfo) SetMiddleName(name string) {
|
|
||||||
u.MiddleName = name
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *userinfo) SetNickname(name string) {
|
|
||||||
u.Nickname = name
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *userinfo) SetUpdatedAt(date time.Time) {
|
|
||||||
u.UpdatedAt = Time(date)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *userinfo) SetProfile(profile string) {
|
|
||||||
u.Profile = profile
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *userinfo) SetPicture(picture string) {
|
|
||||||
u.Picture = picture
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *userinfo) SetWebsite(website string) {
|
|
||||||
u.Website = website
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *userinfo) SetGender(gender Gender) {
|
|
||||||
u.Gender = gender
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *userinfo) SetBirthdate(birthdate string) {
|
|
||||||
u.Birthdate = birthdate
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *userinfo) SetZoneinfo(zoneInfo string) {
|
|
||||||
u.Zoneinfo = zoneInfo
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *userinfo) SetLocale(locale language.Tag) {
|
|
||||||
u.Locale = locale
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *userinfo) SetPreferredUsername(name string) {
|
|
||||||
u.PreferredUsername = name
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *userinfo) SetEmail(email string, verified bool) {
|
|
||||||
u.Email = email
|
|
||||||
u.EmailVerified = boolString(verified)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *userinfo) SetPhone(phone string, verified bool) {
|
|
||||||
u.PhoneNumber = phone
|
|
||||||
u.PhoneNumberVerified = verified
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *userinfo) SetAddress(address UserInfoAddress) {
|
|
||||||
u.Address = address
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *userinfo) AppendClaims(key string, value interface{}) {
|
|
||||||
if u.claims == nil {
|
|
||||||
u.claims = make(map[string]interface{})
|
|
||||||
}
|
|
||||||
u.claims[key] = value
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *userInfoAddress) GetFormatted() string {
|
|
||||||
return u.Formatted
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *userInfoAddress) GetStreetAddress() string {
|
|
||||||
return u.StreetAddress
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *userInfoAddress) GetLocality() string {
|
|
||||||
return u.Locality
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *userInfoAddress) GetRegion() string {
|
|
||||||
return u.Region
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *userInfoAddress) GetPostalCode() string {
|
|
||||||
return u.PostalCode
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *userInfoAddress) GetCountry() string {
|
|
||||||
return u.Country
|
|
||||||
}
|
|
||||||
|
|
||||||
type userInfoProfile struct {
|
|
||||||
Name string `json:"name,omitempty"`
|
Name string `json:"name,omitempty"`
|
||||||
GivenName string `json:"given_name,omitempty"`
|
GivenName string `json:"given_name,omitempty"`
|
||||||
FamilyName string `json:"family_name,omitempty"`
|
FamilyName string `json:"family_name,omitempty"`
|
||||||
|
@ -298,23 +51,23 @@ type userInfoProfile struct {
|
||||||
Gender Gender `json:"gender,omitempty"`
|
Gender Gender `json:"gender,omitempty"`
|
||||||
Birthdate string `json:"birthdate,omitempty"`
|
Birthdate string `json:"birthdate,omitempty"`
|
||||||
Zoneinfo string `json:"zoneinfo,omitempty"`
|
Zoneinfo string `json:"zoneinfo,omitempty"`
|
||||||
Locale language.Tag `json:"locale,omitempty"`
|
Locale *Locale `json:"locale,omitempty"`
|
||||||
UpdatedAt Time `json:"updated_at,omitempty"`
|
UpdatedAt Time `json:"updated_at,omitempty"`
|
||||||
PreferredUsername string `json:"preferred_username,omitempty"`
|
PreferredUsername string `json:"preferred_username,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type userInfoEmail struct {
|
type UserInfoEmail struct {
|
||||||
Email string `json:"email,omitempty"`
|
Email string `json:"email,omitempty"`
|
||||||
|
|
||||||
// Handle providers that return email_verified as a string
|
// Handle providers that return email_verified as a string
|
||||||
// https://forums.aws.amazon.com/thread.jspa?messageID=949441󧳁
|
// https://forums.aws.amazon.com/thread.jspa?messageID=949441󧳁
|
||||||
// https://discuss.elastic.co/t/openid-error-after-authenticating-against-aws-cognito/206018/11
|
// https://discuss.elastic.co/t/openid-error-after-authenticating-against-aws-cognito/206018/11
|
||||||
EmailVerified boolString `json:"email_verified,omitempty"`
|
EmailVerified Bool `json:"email_verified,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type boolString bool
|
type Bool bool
|
||||||
|
|
||||||
func (bs *boolString) UnmarshalJSON(data []byte) error {
|
func (bs *Bool) UnmarshalJSON(data []byte) error {
|
||||||
if string(data) == "true" || string(data) == `"true"` {
|
if string(data) == "true" || string(data) == `"true"` {
|
||||||
*bs = true
|
*bs = true
|
||||||
}
|
}
|
||||||
|
@ -322,12 +75,12 @@ func (bs *boolString) UnmarshalJSON(data []byte) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type userInfoPhone struct {
|
type UserInfoPhone struct {
|
||||||
PhoneNumber string `json:"phone_number,omitempty"`
|
PhoneNumber string `json:"phone_number,omitempty"`
|
||||||
PhoneNumberVerified bool `json:"phone_number_verified,omitempty"`
|
PhoneNumberVerified bool `json:"phone_number_verified,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type userInfoAddress struct {
|
type UserInfoAddress struct {
|
||||||
Formatted string `json:"formatted,omitempty"`
|
Formatted string `json:"formatted,omitempty"`
|
||||||
StreetAddress string `json:"street_address,omitempty"`
|
StreetAddress string `json:"street_address,omitempty"`
|
||||||
Locality string `json:"locality,omitempty"`
|
Locality string `json:"locality,omitempty"`
|
||||||
|
@ -336,76 +89,6 @@ type userInfoAddress struct {
|
||||||
Country string `json:"country,omitempty"`
|
Country string `json:"country,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewUserInfoAddress(streetAddress, locality, region, postalCode, country, formatted string) UserInfoAddress {
|
|
||||||
return &userInfoAddress{
|
|
||||||
StreetAddress: streetAddress,
|
|
||||||
Locality: locality,
|
|
||||||
Region: region,
|
|
||||||
PostalCode: postalCode,
|
|
||||||
Country: country,
|
|
||||||
Formatted: formatted,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *userinfo) MarshalJSON() ([]byte, error) {
|
|
||||||
type Alias userinfo
|
|
||||||
a := &struct {
|
|
||||||
*Alias
|
|
||||||
Locale interface{} `json:"locale,omitempty"`
|
|
||||||
UpdatedAt int64 `json:"updated_at,omitempty"`
|
|
||||||
}{
|
|
||||||
Alias: (*Alias)(u),
|
|
||||||
}
|
|
||||||
if !u.Locale.IsRoot() {
|
|
||||||
a.Locale = u.Locale
|
|
||||||
}
|
|
||||||
if !time.Time(u.UpdatedAt).IsZero() {
|
|
||||||
a.UpdatedAt = time.Time(u.UpdatedAt).Unix()
|
|
||||||
}
|
|
||||||
|
|
||||||
b, err := json.Marshal(a)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(u.claims) == 0 {
|
|
||||||
return b, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
err = json.Unmarshal(b, &u.claims)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("jws: invalid map of custom claims %v", u.claims)
|
|
||||||
}
|
|
||||||
|
|
||||||
return json.Marshal(u.claims)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *userinfo) UnmarshalJSON(data []byte) error {
|
|
||||||
type Alias userinfo
|
|
||||||
a := &struct {
|
|
||||||
Address *userInfoAddress `json:"address,omitempty"`
|
|
||||||
*Alias
|
|
||||||
UpdatedAt int64 `json:"update_at,omitempty"`
|
|
||||||
}{
|
|
||||||
Alias: (*Alias)(u),
|
|
||||||
}
|
|
||||||
if err := json.Unmarshal(data, &a); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if a.Address != nil {
|
|
||||||
u.Address = a.Address
|
|
||||||
}
|
|
||||||
|
|
||||||
u.UpdatedAt = Time(time.Unix(a.UpdatedAt, 0).UTC())
|
|
||||||
|
|
||||||
if err := json.Unmarshal(data, &u.claims); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type UserInfoRequest struct {
|
type UserInfoRequest struct {
|
||||||
AccessToken string `schema:"access_token"`
|
AccessToken string `schema:"access_token"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,21 +7,54 @@ import (
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestUserInfo_AppendClaims(t *testing.T) {
|
||||||
|
u := new(UserInfo)
|
||||||
|
u.AppendClaims("a", "b")
|
||||||
|
want := map[string]any{"a": "b"}
|
||||||
|
assert.Equal(t, want, u.Claims)
|
||||||
|
|
||||||
|
u.AppendClaims("d", "e")
|
||||||
|
want["d"] = "e"
|
||||||
|
assert.Equal(t, want, u.Claims)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUserInfo_GetAddress(t *testing.T) {
|
||||||
|
// nil address
|
||||||
|
u := new(UserInfo)
|
||||||
|
assert.Equal(t, &UserInfoAddress{}, u.GetAddress())
|
||||||
|
|
||||||
|
u.Address = &UserInfoAddress{PostalCode: "1234"}
|
||||||
|
assert.Equal(t, u.Address, u.GetAddress())
|
||||||
|
}
|
||||||
|
|
||||||
func TestUserInfoMarshal(t *testing.T) {
|
func TestUserInfoMarshal(t *testing.T) {
|
||||||
userinfo := NewUserInfo()
|
userinfo := &UserInfo{
|
||||||
userinfo.SetSubject("test")
|
Subject: "test",
|
||||||
userinfo.SetAddress(NewUserInfoAddress("Test 789\nPostfach 2", "", "", "", "", ""))
|
Address: &UserInfoAddress{
|
||||||
userinfo.SetEmail("test", true)
|
StreetAddress: "Test 789\nPostfach 2",
|
||||||
userinfo.SetPhone("0791234567", true)
|
},
|
||||||
userinfo.SetName("Test")
|
UserInfoEmail: UserInfoEmail{
|
||||||
userinfo.AppendClaims("private_claim", "test")
|
Email: "test",
|
||||||
|
EmailVerified: true,
|
||||||
|
},
|
||||||
|
UserInfoPhone: UserInfoPhone{
|
||||||
|
PhoneNumber: "0791234567",
|
||||||
|
PhoneNumberVerified: true,
|
||||||
|
},
|
||||||
|
UserInfoProfile: UserInfoProfile{
|
||||||
|
Name: "Test",
|
||||||
|
},
|
||||||
|
Claims: map[string]any{"private_claim": "test"},
|
||||||
|
}
|
||||||
|
|
||||||
marshal, err := json.Marshal(userinfo)
|
marshal, err := json.Marshal(userinfo)
|
||||||
out := NewUserInfo()
|
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
out := new(UserInfo)
|
||||||
assert.NoError(t, json.Unmarshal(marshal, out))
|
assert.NoError(t, json.Unmarshal(marshal, out))
|
||||||
assert.Equal(t, userinfo.GetAddress(), out.GetAddress())
|
assert.Equal(t, userinfo, out)
|
||||||
expected, err := json.Marshal(out)
|
expected, err := json.Marshal(out)
|
||||||
|
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, expected, marshal)
|
assert.Equal(t, expected, marshal)
|
||||||
}
|
}
|
||||||
|
@ -29,91 +62,55 @@ func TestUserInfoMarshal(t *testing.T) {
|
||||||
func TestUserInfoEmailVerifiedUnmarshal(t *testing.T) {
|
func TestUserInfoEmailVerifiedUnmarshal(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
t.Run("unmarsha email_verified from json bool true", func(t *testing.T) {
|
t.Run("unmarshal email_verified from json bool true", func(t *testing.T) {
|
||||||
jsonBool := []byte(`{"email": "my@email.com", "email_verified": true}`)
|
jsonBool := []byte(`{"email": "my@email.com", "email_verified": true}`)
|
||||||
|
|
||||||
var uie userInfoEmail
|
var uie UserInfoEmail
|
||||||
|
|
||||||
err := json.Unmarshal(jsonBool, &uie)
|
err := json.Unmarshal(jsonBool, &uie)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, userInfoEmail{
|
assert.Equal(t, UserInfoEmail{
|
||||||
Email: "my@email.com",
|
Email: "my@email.com",
|
||||||
EmailVerified: true,
|
EmailVerified: true,
|
||||||
}, uie)
|
}, uie)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("unmarsha email_verified from json string true", func(t *testing.T) {
|
t.Run("unmarshal email_verified from json string true", func(t *testing.T) {
|
||||||
jsonBool := []byte(`{"email": "my@email.com", "email_verified": "true"}`)
|
jsonBool := []byte(`{"email": "my@email.com", "email_verified": "true"}`)
|
||||||
|
|
||||||
var uie userInfoEmail
|
var uie UserInfoEmail
|
||||||
|
|
||||||
err := json.Unmarshal(jsonBool, &uie)
|
err := json.Unmarshal(jsonBool, &uie)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, userInfoEmail{
|
assert.Equal(t, UserInfoEmail{
|
||||||
Email: "my@email.com",
|
Email: "my@email.com",
|
||||||
EmailVerified: true,
|
EmailVerified: true,
|
||||||
}, uie)
|
}, uie)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("unmarsha email_verified from json bool false", func(t *testing.T) {
|
t.Run("unmarshal email_verified from json bool false", func(t *testing.T) {
|
||||||
jsonBool := []byte(`{"email": "my@email.com", "email_verified": false}`)
|
jsonBool := []byte(`{"email": "my@email.com", "email_verified": false}`)
|
||||||
|
|
||||||
var uie userInfoEmail
|
var uie UserInfoEmail
|
||||||
|
|
||||||
err := json.Unmarshal(jsonBool, &uie)
|
err := json.Unmarshal(jsonBool, &uie)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, userInfoEmail{
|
assert.Equal(t, UserInfoEmail{
|
||||||
Email: "my@email.com",
|
Email: "my@email.com",
|
||||||
EmailVerified: false,
|
EmailVerified: false,
|
||||||
}, uie)
|
}, uie)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("unmarsha email_verified from json string false", func(t *testing.T) {
|
t.Run("unmarshal email_verified from json string false", func(t *testing.T) {
|
||||||
jsonBool := []byte(`{"email": "my@email.com", "email_verified": "false"}`)
|
jsonBool := []byte(`{"email": "my@email.com", "email_verified": "false"}`)
|
||||||
|
|
||||||
var uie userInfoEmail
|
var uie UserInfoEmail
|
||||||
|
|
||||||
err := json.Unmarshal(jsonBool, &uie)
|
err := json.Unmarshal(jsonBool, &uie)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, userInfoEmail{
|
assert.Equal(t, UserInfoEmail{
|
||||||
Email: "my@email.com",
|
Email: "my@email.com",
|
||||||
EmailVerified: false,
|
EmailVerified: false,
|
||||||
}, uie)
|
}, uie)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// issue 203 test case.
|
|
||||||
func Test_userinfo_GetAddress_issue_203(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
data string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "with address",
|
|
||||||
data: `{"address":{"street_address":"Test 789\nPostfach 2"},"email":"test","email_verified":true,"name":"Test","phone_number":"0791234567","phone_number_verified":true,"private_claim":"test","sub":"test"}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "without address",
|
|
||||||
data: `{"email":"test","email_verified":true,"name":"Test","phone_number":"0791234567","phone_number_verified":true,"private_claim":"test","sub":"test"}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "null address",
|
|
||||||
data: `{"address":null,"email":"test","email_verified":true,"name":"Test","phone_number":"0791234567","phone_number_verified":true,"private_claim":"test","sub":"test"}`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
info := &userinfo{}
|
|
||||||
err := json.Unmarshal([]byte(tt.data), info)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
info.GetAddress().GetCountry() //<- used to panic
|
|
||||||
|
|
||||||
// now shortly assure that a marshalling still produces the same as was parsed into the struct
|
|
||||||
marshal, err := json.Marshal(info)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, tt.data, string(marshal))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
49
pkg/oidc/util.go
Normal file
49
pkg/oidc/util.go
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
package oidc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// mergeAndMarshalClaims merges registered and the custom
|
||||||
|
// claims map into a single JSON object.
|
||||||
|
// Registered fields overwrite custom claims.
|
||||||
|
func mergeAndMarshalClaims(registered any, claims map[string]any) ([]byte, error) {
|
||||||
|
// Use a buffer for memory re-use, instead off letting
|
||||||
|
// json allocate a new []byte for every step.
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
|
||||||
|
// Marshal the registered claims into JSON
|
||||||
|
if err := json.NewEncoder(buf).Encode(registered); err != nil {
|
||||||
|
return nil, fmt.Errorf("oidc registered claims: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(claims) > 0 {
|
||||||
|
// Merge JSON data into custom claims.
|
||||||
|
// The full-read action by the decoder resets the buffer
|
||||||
|
// to zero len, while retaining underlaying cap.
|
||||||
|
if err := json.NewDecoder(buf).Decode(&claims); err != nil {
|
||||||
|
return nil, fmt.Errorf("oidc registered claims: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marshal the final result.
|
||||||
|
if err := json.NewEncoder(buf).Encode(claims); err != nil {
|
||||||
|
return nil, fmt.Errorf("oidc custom claims: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// unmarshalJSONMulti unmarshals the same JSON data into multiple destinations.
|
||||||
|
// Each destination must be a pointer, as per json.Unmarshal rules.
|
||||||
|
// Returns on the first error and destinations may be partly filled with data.
|
||||||
|
func unmarshalJSONMulti(data []byte, destinations ...any) error {
|
||||||
|
for _, dst := range destinations {
|
||||||
|
if err := json.Unmarshal(data, dst); err != nil {
|
||||||
|
return fmt.Errorf("oidc: %w into %T", err, dst)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
147
pkg/oidc/util_test.go
Normal file
147
pkg/oidc/util_test.go
Normal file
|
@ -0,0 +1,147 @@
|
||||||
|
package oidc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
type jsonErrorTest struct{}
|
||||||
|
|
||||||
|
func (jsonErrorTest) MarshalJSON() ([]byte, error) {
|
||||||
|
return nil, errors.New("test")
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_mergeAndMarshalClaims(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
registered any
|
||||||
|
claims map[string]any
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
want string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "encoder error",
|
||||||
|
args: args{
|
||||||
|
registered: jsonErrorTest{},
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no claims",
|
||||||
|
args: args{
|
||||||
|
registered: struct {
|
||||||
|
Foo string `json:"foo,omitempty"`
|
||||||
|
}{
|
||||||
|
Foo: "bar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: "{\"foo\":\"bar\"}\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with claims",
|
||||||
|
args: args{
|
||||||
|
registered: struct {
|
||||||
|
Foo string `json:"foo,omitempty"`
|
||||||
|
}{
|
||||||
|
Foo: "bar",
|
||||||
|
},
|
||||||
|
claims: map[string]any{
|
||||||
|
"bar": "foo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: "{\"bar\":\"foo\",\"foo\":\"bar\"}\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "registered overwrites custom",
|
||||||
|
args: args{
|
||||||
|
registered: struct {
|
||||||
|
Foo string `json:"foo,omitempty"`
|
||||||
|
}{
|
||||||
|
Foo: "bar",
|
||||||
|
},
|
||||||
|
claims: map[string]any{
|
||||||
|
"foo": "Hello, World!",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: "{\"foo\":\"bar\"}\n",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got, err := mergeAndMarshalClaims(tt.args.registered, tt.args.claims)
|
||||||
|
if tt.wantErr {
|
||||||
|
require.Error(t, err)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
assert.Equal(t, tt.want, string(got))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_unmarshalJSONMulti(t *testing.T) {
|
||||||
|
type dst struct {
|
||||||
|
Foo string `json:"foo,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type args struct {
|
||||||
|
data string
|
||||||
|
destinations []any
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
want []any
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "error",
|
||||||
|
args: args{
|
||||||
|
data: "~!~~",
|
||||||
|
destinations: []any{
|
||||||
|
&dst{},
|
||||||
|
&map[string]any{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: []any{
|
||||||
|
&dst{},
|
||||||
|
&map[string]any{},
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "success",
|
||||||
|
args: args{
|
||||||
|
data: "{\"bar\":\"foo\",\"foo\":\"bar\"}\n",
|
||||||
|
destinations: []any{
|
||||||
|
&dst{},
|
||||||
|
&map[string]any{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: []any{
|
||||||
|
&dst{Foo: "bar"},
|
||||||
|
&map[string]any{
|
||||||
|
"foo": "bar",
|
||||||
|
"bar": "foo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
err := unmarshalJSONMulti([]byte(tt.args.data), tt.args.destinations...)
|
||||||
|
if tt.wantErr {
|
||||||
|
require.Error(t, err)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
assert.Equal(t, tt.want, tt.args.destinations)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,7 +12,7 @@ import (
|
||||||
|
|
||||||
"gopkg.in/square/go-jose.v2"
|
"gopkg.in/square/go-jose.v2"
|
||||||
|
|
||||||
str "github.com/zitadel/oidc/pkg/strings"
|
str "github.com/zitadel/oidc/v2/pkg/strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Claims interface {
|
type Claims interface {
|
||||||
|
@ -32,6 +32,12 @@ type ClaimsSignature interface {
|
||||||
SetSignatureAlgorithm(algorithm jose.SignatureAlgorithm)
|
SetSignatureAlgorithm(algorithm jose.SignatureAlgorithm)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type IDClaims interface {
|
||||||
|
Claims
|
||||||
|
GetSignatureAlgorithm() jose.SignatureAlgorithm
|
||||||
|
GetAccessTokenHash() string
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrParse = errors.New("parsing of request failed")
|
ErrParse = errors.New("parsing of request failed")
|
||||||
ErrIssuerInvalid = errors.New("issuer does not match")
|
ErrIssuerInvalid = errors.New("issuer does not match")
|
||||||
|
|
|
@ -12,9 +12,9 @@ import (
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
|
|
||||||
httphelper "github.com/zitadel/oidc/pkg/http"
|
httphelper "github.com/zitadel/oidc/v2/pkg/http"
|
||||||
"github.com/zitadel/oidc/pkg/oidc"
|
"github.com/zitadel/oidc/v2/pkg/oidc"
|
||||||
str "github.com/zitadel/oidc/pkg/strings"
|
str "github.com/zitadel/oidc/v2/pkg/strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
type AuthRequest interface {
|
type AuthRequest interface {
|
||||||
|
@ -39,10 +39,8 @@ type Authorizer interface {
|
||||||
Storage() Storage
|
Storage() Storage
|
||||||
Decoder() httphelper.Decoder
|
Decoder() httphelper.Decoder
|
||||||
Encoder() httphelper.Encoder
|
Encoder() httphelper.Encoder
|
||||||
Signer() Signer
|
IDTokenHintVerifier(context.Context) IDTokenHintVerifier
|
||||||
IDTokenHintVerifier() IDTokenHintVerifier
|
|
||||||
Crypto() Crypto
|
Crypto() Crypto
|
||||||
Issuer() string
|
|
||||||
RequestObjectSupported() bool
|
RequestObjectSupported() bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -73,8 +71,9 @@ func Authorize(w http.ResponseWriter, r *http.Request, authorizer Authorizer) {
|
||||||
AuthRequestError(w, r, authReq, err, authorizer.Encoder())
|
AuthRequestError(w, r, authReq, err, authorizer.Encoder())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
ctx := r.Context()
|
||||||
if authReq.RequestParam != "" && authorizer.RequestObjectSupported() {
|
if authReq.RequestParam != "" && authorizer.RequestObjectSupported() {
|
||||||
authReq, err = ParseRequestObject(r.Context(), authReq, authorizer.Storage(), authorizer.Issuer())
|
authReq, err = ParseRequestObject(ctx, authReq, authorizer.Storage(), IssuerFromContext(ctx))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
AuthRequestError(w, r, authReq, err, authorizer.Encoder())
|
AuthRequestError(w, r, authReq, err, authorizer.Encoder())
|
||||||
return
|
return
|
||||||
|
@ -92,7 +91,7 @@ func Authorize(w http.ResponseWriter, r *http.Request, authorizer Authorizer) {
|
||||||
if validater, ok := authorizer.(AuthorizeValidator); ok {
|
if validater, ok := authorizer.(AuthorizeValidator); ok {
|
||||||
validation = validater.ValidateAuthRequest
|
validation = validater.ValidateAuthRequest
|
||||||
}
|
}
|
||||||
userID, err := validation(r.Context(), authReq, authorizer.Storage(), authorizer.IDTokenHintVerifier())
|
userID, err := validation(ctx, authReq, authorizer.Storage(), authorizer.IDTokenHintVerifier(ctx))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
AuthRequestError(w, r, authReq, err, authorizer.Encoder())
|
AuthRequestError(w, r, authReq, err, authorizer.Encoder())
|
||||||
return
|
return
|
||||||
|
@ -101,12 +100,12 @@ func Authorize(w http.ResponseWriter, r *http.Request, authorizer Authorizer) {
|
||||||
AuthRequestError(w, r, authReq, oidc.ErrRequestNotSupported(), authorizer.Encoder())
|
AuthRequestError(w, r, authReq, oidc.ErrRequestNotSupported(), authorizer.Encoder())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
req, err := authorizer.Storage().CreateAuthRequest(r.Context(), authReq, userID)
|
req, err := authorizer.Storage().CreateAuthRequest(ctx, authReq, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
AuthRequestError(w, r, authReq, oidc.DefaultToServerError(err, "unable to save auth request"), authorizer.Encoder())
|
AuthRequestError(w, r, authReq, oidc.DefaultToServerError(err, "unable to save auth request"), authorizer.Encoder())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
client, err := authorizer.Storage().GetClientByClientID(r.Context(), req.GetClientID())
|
client, err := authorizer.Storage().GetClientByClientID(ctx, req.GetClientID())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
AuthRequestError(w, r, req, oidc.DefaultToServerError(err, "unable to retrieve client by id"), authorizer.Encoder())
|
AuthRequestError(w, r, req, oidc.DefaultToServerError(err, "unable to retrieve client by id"), authorizer.Encoder())
|
||||||
return
|
return
|
||||||
|
@ -390,7 +389,7 @@ func ValidateAuthReqIDTokenHint(ctx context.Context, idTokenHint string, verifie
|
||||||
if idTokenHint == "" {
|
if idTokenHint == "" {
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
claims, err := VerifyIDTokenHint(ctx, idTokenHint, verifier)
|
claims, err := VerifyIDTokenHint[*oidc.TokenClaims](ctx, idTokenHint, verifier)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", oidc.ErrLoginRequired().WithDescription("The id_token_hint is invalid. " +
|
return "", oidc.ErrLoginRequired().WithDescription("The id_token_hint is invalid. " +
|
||||||
"If you have any questions, you may contact the administrator of the application.")
|
"If you have any questions, you may contact the administrator of the application.")
|
||||||
|
|
|
@ -13,10 +13,10 @@ import (
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
httphelper "github.com/zitadel/oidc/pkg/http"
|
httphelper "github.com/zitadel/oidc/v2/pkg/http"
|
||||||
"github.com/zitadel/oidc/pkg/oidc"
|
"github.com/zitadel/oidc/v2/pkg/oidc"
|
||||||
"github.com/zitadel/oidc/pkg/op"
|
"github.com/zitadel/oidc/v2/pkg/op"
|
||||||
"github.com/zitadel/oidc/pkg/op/mock"
|
"github.com/zitadel/oidc/v2/pkg/op/mock"
|
||||||
)
|
)
|
||||||
|
|
||||||
//
|
//
|
||||||
|
|
100
pkg/op/client.go
100
pkg/op/client.go
|
@ -1,13 +1,19 @@
|
||||||
package op
|
package op
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/zitadel/oidc/pkg/oidc"
|
httphelper "github.com/zitadel/oidc/v2/pkg/http"
|
||||||
|
"github.com/zitadel/oidc/v2/pkg/oidc"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:generate go get github.com/dmarkham/enumer
|
//go:generate go get github.com/dmarkham/enumer
|
||||||
//go:generate go run github.com/dmarkham/enumer -linecomment -sql -json -text -yaml -gqlgen -type=ApplicationType,AccessTokenType
|
//go:generate go run github.com/dmarkham/enumer -linecomment -sql -json -text -yaml -gqlgen -type=ApplicationType,AccessTokenType
|
||||||
|
//go:generate go mod tidy
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ApplicationTypeWeb ApplicationType = iota // web
|
ApplicationTypeWeb ApplicationType = iota // web
|
||||||
|
@ -67,3 +73,95 @@ func ContainsResponseType(types []oidc.ResponseType, responseType oidc.ResponseT
|
||||||
func IsConfidentialType(c Client) bool {
|
func IsConfidentialType(c Client) bool {
|
||||||
return c.ApplicationType() == ApplicationTypeWeb
|
return c.ApplicationType() == ApplicationTypeWeb
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrInvalidAuthHeader = errors.New("invalid basic auth header")
|
||||||
|
ErrNoClientCredentials = errors.New("no client credentials provided")
|
||||||
|
ErrMissingClientID = errors.New("client_id missing from request")
|
||||||
|
)
|
||||||
|
|
||||||
|
type ClientJWTProfile interface {
|
||||||
|
JWTProfileVerifier(context.Context) JWTProfileVerifier
|
||||||
|
}
|
||||||
|
|
||||||
|
func ClientJWTAuth(ctx context.Context, ca oidc.ClientAssertionParams, verifier ClientJWTProfile) (clientID string, err error) {
|
||||||
|
if ca.ClientAssertion == "" {
|
||||||
|
return "", oidc.ErrInvalidClient().WithParent(ErrNoClientCredentials)
|
||||||
|
}
|
||||||
|
|
||||||
|
profile, err := VerifyJWTAssertion(ctx, ca.ClientAssertion, verifier.JWTProfileVerifier(ctx))
|
||||||
|
if err != nil {
|
||||||
|
return "", oidc.ErrUnauthorizedClient().WithParent(err).WithDescription("JWT assertion failed")
|
||||||
|
}
|
||||||
|
return profile.Issuer, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ClientBasicAuth(r *http.Request, storage Storage) (clientID string, err error) {
|
||||||
|
clientID, clientSecret, ok := r.BasicAuth()
|
||||||
|
if !ok {
|
||||||
|
return "", oidc.ErrInvalidClient().WithParent(ErrNoClientCredentials)
|
||||||
|
}
|
||||||
|
clientID, err = url.QueryUnescape(clientID)
|
||||||
|
if err != nil {
|
||||||
|
return "", oidc.ErrInvalidClient().WithParent(ErrInvalidAuthHeader)
|
||||||
|
}
|
||||||
|
clientSecret, err = url.QueryUnescape(clientSecret)
|
||||||
|
if err != nil {
|
||||||
|
return "", oidc.ErrInvalidClient().WithParent(ErrInvalidAuthHeader)
|
||||||
|
}
|
||||||
|
if err := storage.AuthorizeClientIDSecret(r.Context(), clientID, clientSecret); err != nil {
|
||||||
|
return "", oidc.ErrUnauthorizedClient().WithParent(err)
|
||||||
|
}
|
||||||
|
return clientID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClientProvider interface {
|
||||||
|
Decoder() httphelper.Decoder
|
||||||
|
Storage() Storage
|
||||||
|
}
|
||||||
|
|
||||||
|
type clientData struct {
|
||||||
|
ClientID string `schema:"client_id"`
|
||||||
|
oidc.ClientAssertionParams
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClientIDFromRequest parses the request form and tries to obtain the client ID
|
||||||
|
// and reports if it is authenticated, using a JWT or static client secrets over
|
||||||
|
// http basic auth.
|
||||||
|
//
|
||||||
|
// If the Provider implements IntrospectorJWTProfile and "client_assertion" is
|
||||||
|
// present in the form data, JWT assertion will be verified and the
|
||||||
|
// client ID is taken from there.
|
||||||
|
// If any of them is absent, basic auth is attempted.
|
||||||
|
// In absence of basic auth data, the unauthenticated client id from the form
|
||||||
|
// data is returned.
|
||||||
|
//
|
||||||
|
// If no client id can be obtained by any method, oidc.ErrInvalidClient
|
||||||
|
// is returned with ErrMissingClientID wrapped in it.
|
||||||
|
func ClientIDFromRequest(r *http.Request, p ClientProvider) (clientID string, authenticated bool, err error) {
|
||||||
|
err = r.ParseForm()
|
||||||
|
if err != nil {
|
||||||
|
return "", false, oidc.ErrInvalidRequest().WithDescription("cannot parse form").WithParent(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
data := new(clientData)
|
||||||
|
if err = p.Decoder().Decode(data, r.PostForm); err != nil {
|
||||||
|
return "", false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
JWTProfile, ok := p.(ClientJWTProfile)
|
||||||
|
if ok {
|
||||||
|
clientID, err = ClientJWTAuth(r.Context(), data.ClientAssertionParams, JWTProfile)
|
||||||
|
}
|
||||||
|
if !ok || errors.Is(err, ErrNoClientCredentials) {
|
||||||
|
clientID, err = ClientBasicAuth(r, p.Storage())
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
return clientID, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if data.ClientID == "" {
|
||||||
|
return "", false, oidc.ErrInvalidClient().WithParent(ErrMissingClientID)
|
||||||
|
}
|
||||||
|
return data.ClientID, false, nil
|
||||||
|
}
|
||||||
|
|
253
pkg/op/client_test.go
Normal file
253
pkg/op/client_test.go
Normal file
|
@ -0,0 +1,253 @@
|
||||||
|
package op_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/golang/mock/gomock"
|
||||||
|
"github.com/gorilla/schema"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
httphelper "github.com/zitadel/oidc/v2/pkg/http"
|
||||||
|
"github.com/zitadel/oidc/v2/pkg/oidc"
|
||||||
|
"github.com/zitadel/oidc/v2/pkg/op"
|
||||||
|
"github.com/zitadel/oidc/v2/pkg/op/mock"
|
||||||
|
)
|
||||||
|
|
||||||
|
type testClientJWTProfile struct{}
|
||||||
|
|
||||||
|
func (testClientJWTProfile) JWTProfileVerifier(context.Context) op.JWTProfileVerifier { return nil }
|
||||||
|
|
||||||
|
func TestClientJWTAuth(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
ctx context.Context
|
||||||
|
ca oidc.ClientAssertionParams
|
||||||
|
verifier op.ClientJWTProfile
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
wantClientID string
|
||||||
|
wantErr error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "empty assertion",
|
||||||
|
args: args{
|
||||||
|
context.Background(),
|
||||||
|
oidc.ClientAssertionParams{},
|
||||||
|
testClientJWTProfile{},
|
||||||
|
},
|
||||||
|
wantErr: op.ErrNoClientCredentials,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "verification error",
|
||||||
|
args: args{
|
||||||
|
context.Background(),
|
||||||
|
oidc.ClientAssertionParams{
|
||||||
|
ClientAssertion: "foo",
|
||||||
|
},
|
||||||
|
testClientJWTProfile{},
|
||||||
|
},
|
||||||
|
wantErr: oidc.ErrParse,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
gotClientID, err := op.ClientJWTAuth(tt.args.ctx, tt.args.ca, tt.args.verifier)
|
||||||
|
require.ErrorIs(t, err, tt.wantErr)
|
||||||
|
assert.Equal(t, tt.wantClientID, gotClientID)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClientBasicAuth(t *testing.T) {
|
||||||
|
errWrong := errors.New("wrong secret")
|
||||||
|
|
||||||
|
type args struct {
|
||||||
|
username string
|
||||||
|
password string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args *args
|
||||||
|
storage op.Storage
|
||||||
|
wantClientID string
|
||||||
|
wantErr error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "no args",
|
||||||
|
wantErr: op.ErrNoClientCredentials,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "username unescape err",
|
||||||
|
args: &args{
|
||||||
|
username: "%",
|
||||||
|
password: "bar",
|
||||||
|
},
|
||||||
|
wantErr: op.ErrInvalidAuthHeader,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "password unescape err",
|
||||||
|
args: &args{
|
||||||
|
username: "foo",
|
||||||
|
password: "%",
|
||||||
|
},
|
||||||
|
wantErr: op.ErrInvalidAuthHeader,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "auth error",
|
||||||
|
args: &args{
|
||||||
|
username: "foo",
|
||||||
|
password: "wrong",
|
||||||
|
},
|
||||||
|
storage: func() op.Storage {
|
||||||
|
s := mock.NewMockStorage(gomock.NewController(t))
|
||||||
|
s.EXPECT().AuthorizeClientIDSecret(context.Background(), "foo", "wrong").Return(errWrong)
|
||||||
|
return s
|
||||||
|
}(),
|
||||||
|
wantErr: errWrong,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "auth error",
|
||||||
|
args: &args{
|
||||||
|
username: "foo",
|
||||||
|
password: "bar",
|
||||||
|
},
|
||||||
|
storage: func() op.Storage {
|
||||||
|
s := mock.NewMockStorage(gomock.NewController(t))
|
||||||
|
s.EXPECT().AuthorizeClientIDSecret(context.Background(), "foo", "bar").Return(nil)
|
||||||
|
return s
|
||||||
|
}(),
|
||||||
|
wantClientID: "foo",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
r := httptest.NewRequest(http.MethodGet, "/foo", nil)
|
||||||
|
if tt.args != nil {
|
||||||
|
r.SetBasicAuth(tt.args.username, tt.args.password)
|
||||||
|
}
|
||||||
|
|
||||||
|
gotClientID, err := op.ClientBasicAuth(r, tt.storage)
|
||||||
|
require.ErrorIs(t, err, tt.wantErr)
|
||||||
|
assert.Equal(t, tt.wantClientID, gotClientID)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type errReader struct{}
|
||||||
|
|
||||||
|
func (errReader) Read([]byte) (int, error) {
|
||||||
|
return 0, io.ErrNoProgress
|
||||||
|
}
|
||||||
|
|
||||||
|
type testClientProvider struct {
|
||||||
|
storage op.Storage
|
||||||
|
}
|
||||||
|
|
||||||
|
func (testClientProvider) Decoder() httphelper.Decoder {
|
||||||
|
return schema.NewDecoder()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p testClientProvider) Storage() op.Storage {
|
||||||
|
return p.storage
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClientIDFromRequest(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
body io.Reader
|
||||||
|
p op.ClientProvider
|
||||||
|
}
|
||||||
|
type basicAuth struct {
|
||||||
|
username string
|
||||||
|
password string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
basicAuth *basicAuth
|
||||||
|
wantClientID string
|
||||||
|
wantAuthenticated bool
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "parse error",
|
||||||
|
args: args{
|
||||||
|
body: errReader{},
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unauthenticated",
|
||||||
|
args: args{
|
||||||
|
body: strings.NewReader(
|
||||||
|
url.Values{
|
||||||
|
"client_id": []string{"foo"},
|
||||||
|
}.Encode(),
|
||||||
|
),
|
||||||
|
p: testClientProvider{
|
||||||
|
storage: mock.NewStorage(t),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantClientID: "foo",
|
||||||
|
wantAuthenticated: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "authenticated",
|
||||||
|
args: args{
|
||||||
|
body: strings.NewReader(
|
||||||
|
url.Values{}.Encode(),
|
||||||
|
),
|
||||||
|
p: testClientProvider{
|
||||||
|
storage: func() op.Storage {
|
||||||
|
s := mock.NewMockStorage(gomock.NewController(t))
|
||||||
|
s.EXPECT().AuthorizeClientIDSecret(context.Background(), "foo", "bar").Return(nil)
|
||||||
|
return s
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
basicAuth: &basicAuth{
|
||||||
|
username: "foo",
|
||||||
|
password: "bar",
|
||||||
|
},
|
||||||
|
wantClientID: "foo",
|
||||||
|
wantAuthenticated: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing client id",
|
||||||
|
args: args{
|
||||||
|
body: strings.NewReader(
|
||||||
|
url.Values{}.Encode(),
|
||||||
|
),
|
||||||
|
p: testClientProvider{
|
||||||
|
storage: mock.NewStorage(t),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
r := httptest.NewRequest(http.MethodPost, "/foo", tt.args.body)
|
||||||
|
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
if tt.basicAuth != nil {
|
||||||
|
r.SetBasicAuth(tt.basicAuth.username, tt.basicAuth.password)
|
||||||
|
}
|
||||||
|
|
||||||
|
gotClientID, gotAuthenticated, err := op.ClientIDFromRequest(r, tt.args.p)
|
||||||
|
if tt.wantErr {
|
||||||
|
require.Error(t, err)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
assert.Equal(t, tt.wantClientID, gotClientID)
|
||||||
|
assert.Equal(t, tt.wantAuthenticated, gotAuthenticated)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,20 +2,24 @@ package op
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"strings"
|
||||||
|
|
||||||
"golang.org/x/text/language"
|
"golang.org/x/text/language"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
var (
|
||||||
OidcDevMode = "ZITADEL_OIDC_DEV"
|
ErrInvalidIssuerPath = errors.New("no fragments or query allowed for issuer")
|
||||||
// deprecated: use OidcDevMode (ZITADEL_OIDC_DEV=true)
|
ErrInvalidIssuerNoIssuer = errors.New("missing issuer")
|
||||||
devMode = "CAOS_OIDC_DEV"
|
ErrInvalidIssuerURL = errors.New("invalid url for issuer")
|
||||||
|
ErrInvalidIssuerMissingHost = errors.New("host for issuer missing")
|
||||||
|
ErrInvalidIssuerHTTPS = errors.New("scheme for issuer must be `https`")
|
||||||
)
|
)
|
||||||
|
|
||||||
type Configuration interface {
|
type Configuration interface {
|
||||||
Issuer() string
|
IssuerFromRequest(r *http.Request) string
|
||||||
|
Insecure() bool
|
||||||
AuthorizationEndpoint() Endpoint
|
AuthorizationEndpoint() Endpoint
|
||||||
TokenEndpoint() Endpoint
|
TokenEndpoint() Endpoint
|
||||||
IntrospectionEndpoint() Endpoint
|
IntrospectionEndpoint() Endpoint
|
||||||
|
@ -23,6 +27,7 @@ type Configuration interface {
|
||||||
RevocationEndpoint() Endpoint
|
RevocationEndpoint() Endpoint
|
||||||
EndSessionEndpoint() Endpoint
|
EndSessionEndpoint() Endpoint
|
||||||
KeysEndpoint() Endpoint
|
KeysEndpoint() Endpoint
|
||||||
|
DeviceAuthorizationEndpoint() Endpoint
|
||||||
|
|
||||||
AuthMethodPostSupported() bool
|
AuthMethodPostSupported() bool
|
||||||
CodeMethodS256Supported() bool
|
CodeMethodS256Supported() bool
|
||||||
|
@ -32,6 +37,7 @@ type Configuration interface {
|
||||||
GrantTypeTokenExchangeSupported() bool
|
GrantTypeTokenExchangeSupported() bool
|
||||||
GrantTypeJWTAuthorizationSupported() bool
|
GrantTypeJWTAuthorizationSupported() bool
|
||||||
GrantTypeClientCredentialsSupported() bool
|
GrantTypeClientCredentialsSupported() bool
|
||||||
|
GrantTypeDeviceCodeSupported() bool
|
||||||
IntrospectionAuthMethodPrivateKeyJWTSupported() bool
|
IntrospectionAuthMethodPrivateKeyJWTSupported() bool
|
||||||
IntrospectionEndpointSigningAlgorithmsSupported() []string
|
IntrospectionEndpointSigningAlgorithmsSupported() []string
|
||||||
RevocationAuthMethodPrivateKeyJWTSupported() bool
|
RevocationAuthMethodPrivateKeyJWTSupported() bool
|
||||||
|
@ -40,38 +46,77 @@ type Configuration interface {
|
||||||
RequestObjectSigningAlgorithmsSupported() []string
|
RequestObjectSigningAlgorithmsSupported() []string
|
||||||
|
|
||||||
SupportedUILocales() []language.Tag
|
SupportedUILocales() []language.Tag
|
||||||
|
DeviceAuthorization() DeviceAuthorizationConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
func ValidateIssuer(issuer string) error {
|
type IssuerFromRequest func(r *http.Request) string
|
||||||
|
|
||||||
|
func IssuerFromHost(path string) func(bool) (IssuerFromRequest, error) {
|
||||||
|
return func(allowInsecure bool) (IssuerFromRequest, error) {
|
||||||
|
issuerPath, err := url.Parse(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, ErrInvalidIssuerURL
|
||||||
|
}
|
||||||
|
if err := ValidateIssuerPath(issuerPath); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return func(r *http.Request) string {
|
||||||
|
return dynamicIssuer(r.Host, path, allowInsecure)
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func StaticIssuer(issuer string) func(bool) (IssuerFromRequest, error) {
|
||||||
|
return func(allowInsecure bool) (IssuerFromRequest, error) {
|
||||||
|
if err := ValidateIssuer(issuer, allowInsecure); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return func(_ *http.Request) string {
|
||||||
|
return issuer
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ValidateIssuer(issuer string, allowInsecure bool) error {
|
||||||
if issuer == "" {
|
if issuer == "" {
|
||||||
return errors.New("missing issuer")
|
return ErrInvalidIssuerNoIssuer
|
||||||
}
|
}
|
||||||
u, err := url.Parse(issuer)
|
u, err := url.Parse(issuer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.New("invalid url for issuer")
|
return ErrInvalidIssuerURL
|
||||||
}
|
}
|
||||||
if u.Host == "" {
|
if u.Host == "" {
|
||||||
return errors.New("host for issuer missing")
|
return ErrInvalidIssuerMissingHost
|
||||||
}
|
}
|
||||||
if u.Scheme != "https" {
|
if u.Scheme != "https" {
|
||||||
if !devLocalAllowed(u) {
|
if !devLocalAllowed(u, allowInsecure) {
|
||||||
return errors.New("scheme for issuer must be `https`")
|
return ErrInvalidIssuerHTTPS
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if u.Fragment != "" || len(u.Query()) > 0 {
|
return ValidateIssuerPath(u)
|
||||||
return errors.New("no fragments or query allowed for issuer")
|
}
|
||||||
|
|
||||||
|
func ValidateIssuerPath(issuer *url.URL) error {
|
||||||
|
if issuer.Fragment != "" || len(issuer.Query()) > 0 {
|
||||||
|
return ErrInvalidIssuerPath
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func devLocalAllowed(url *url.URL) bool {
|
func devLocalAllowed(url *url.URL, allowInsecure bool) bool {
|
||||||
_, b := os.LookupEnv(OidcDevMode)
|
if !allowInsecure {
|
||||||
if !b {
|
return false
|
||||||
// check the old / current env var as well
|
|
||||||
_, b = os.LookupEnv(devMode)
|
|
||||||
if !b {
|
|
||||||
return b
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return url.Scheme == "http"
|
return url.Scheme == "http"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func dynamicIssuer(issuer, path string, allowInsecure bool) string {
|
||||||
|
schema := "https"
|
||||||
|
if allowInsecure {
|
||||||
|
schema = "http"
|
||||||
|
}
|
||||||
|
if len(path) > 0 && !strings.HasPrefix(path, "/") {
|
||||||
|
path = "/" + path
|
||||||
|
}
|
||||||
|
return schema + "://" + issuer + path
|
||||||
|
}
|
||||||
|
|
|
@ -1,13 +1,17 @@
|
||||||
package op
|
package op
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestValidateIssuer(t *testing.T) {
|
func TestValidateIssuer(t *testing.T) {
|
||||||
type args struct {
|
type args struct {
|
||||||
issuer string
|
issuer string
|
||||||
|
allowInsecure bool
|
||||||
}
|
}
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
@ -16,65 +20,97 @@ func TestValidateIssuer(t *testing.T) {
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
"missing issuer fails",
|
"missing issuer fails",
|
||||||
args{""},
|
args{
|
||||||
|
issuer: "",
|
||||||
|
},
|
||||||
true,
|
true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"invalid url for issuer fails",
|
"invalid url for issuer fails",
|
||||||
args{":issuer"},
|
args{
|
||||||
true,
|
issuer: ":issuer",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"invalid url for issuer fails",
|
|
||||||
args{":issuer"},
|
|
||||||
true,
|
true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"host for issuer missing fails",
|
"host for issuer missing fails",
|
||||||
args{"https:///issuer"},
|
args{
|
||||||
true,
|
issuer: "https:///issuer",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"host for not https fails",
|
|
||||||
args{"http://issuer.com"},
|
|
||||||
true,
|
true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"host with fragment fails",
|
"host with fragment fails",
|
||||||
args{"https://issuer.com/#issuer"},
|
args{
|
||||||
|
issuer: "https://issuer.com/#issuer",
|
||||||
|
},
|
||||||
true,
|
true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"host with query fails",
|
"host with query fails",
|
||||||
args{"https://issuer.com?issuer=me"},
|
args{
|
||||||
|
issuer: "https://issuer.com?issuer=me",
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"host with http fails",
|
||||||
|
args{
|
||||||
|
issuer: "http://issuer.com",
|
||||||
|
},
|
||||||
true,
|
true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"host with https ok",
|
"host with https ok",
|
||||||
args{"https://issuer.com"},
|
args{
|
||||||
|
issuer: "https://issuer.com",
|
||||||
|
},
|
||||||
false,
|
false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"localhost with http fails",
|
"custom scheme fails",
|
||||||
args{"http://localhost:9999"},
|
args{
|
||||||
|
issuer: "custom://localhost:9999",
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"http with allowInsecure ok",
|
||||||
|
args{
|
||||||
|
issuer: "http://localhost:9999",
|
||||||
|
allowInsecure: true,
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"https with allowInsecure ok",
|
||||||
|
args{
|
||||||
|
issuer: "https://localhost:9999",
|
||||||
|
allowInsecure: true,
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"custom scheme with allowInsecure fails",
|
||||||
|
args{
|
||||||
|
issuer: "custom://localhost:9999",
|
||||||
|
allowInsecure: true,
|
||||||
|
},
|
||||||
true,
|
true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
// ensure env is not set
|
|
||||||
//nolint:errcheck
|
|
||||||
os.Unsetenv(OidcDevMode)
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
if err := ValidateIssuer(tt.args.issuer); (err != nil) != tt.wantErr {
|
if err := ValidateIssuer(tt.args.issuer, tt.args.allowInsecure); (err != nil) != tt.wantErr {
|
||||||
t.Errorf("ValidateIssuer() error = %v, wantErr %v", err, tt.wantErr)
|
t.Errorf("ValidateIssuer() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestValidateIssuerDevLocalAllowed(t *testing.T) {
|
func TestValidateIssuerPath(t *testing.T) {
|
||||||
type args struct {
|
type args struct {
|
||||||
issuer string
|
issuerPath *url.URL
|
||||||
}
|
}
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
@ -82,17 +118,217 @@ func TestValidateIssuerDevLocalAllowed(t *testing.T) {
|
||||||
wantErr bool
|
wantErr bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
"localhost with http with dev ok",
|
"empty ok",
|
||||||
args{"http://localhost:9999"},
|
args{func() *url.URL {
|
||||||
|
u, _ := url.Parse("")
|
||||||
|
return u
|
||||||
|
}()},
|
||||||
false,
|
false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"custom ok",
|
||||||
|
args{func() *url.URL {
|
||||||
|
u, _ := url.Parse("/custom")
|
||||||
|
return u
|
||||||
|
}()},
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fragment fails",
|
||||||
|
args{func() *url.URL {
|
||||||
|
u, _ := url.Parse("#fragment")
|
||||||
|
return u
|
||||||
|
}()},
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"query fails",
|
||||||
|
args{func() *url.URL {
|
||||||
|
u, _ := url.Parse("?query=value")
|
||||||
|
return u
|
||||||
|
}()},
|
||||||
|
true,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
//nolint:errcheck
|
|
||||||
os.Setenv(OidcDevMode, "true")
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
if err := ValidateIssuer(tt.args.issuer); (err != nil) != tt.wantErr {
|
if err := ValidateIssuerPath(tt.args.issuerPath); (err != nil) != tt.wantErr {
|
||||||
t.Errorf("ValidateIssuer() error = %v, wantErr %v", err, tt.wantErr)
|
t.Errorf("ValidateIssuerPath() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIssuerFromHost(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
path string
|
||||||
|
allowInsecure bool
|
||||||
|
target string
|
||||||
|
}
|
||||||
|
type res struct {
|
||||||
|
issuer string
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
res res
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"invalid issuer path",
|
||||||
|
args{
|
||||||
|
path: "/#fragment",
|
||||||
|
allowInsecure: false,
|
||||||
|
},
|
||||||
|
res{
|
||||||
|
issuer: "",
|
||||||
|
err: ErrInvalidIssuerPath,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"empty path secure",
|
||||||
|
args{
|
||||||
|
path: "",
|
||||||
|
allowInsecure: false,
|
||||||
|
target: "https://issuer.com",
|
||||||
|
},
|
||||||
|
res{
|
||||||
|
issuer: "https://issuer.com",
|
||||||
|
err: nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"custom path secure",
|
||||||
|
args{
|
||||||
|
path: "/custom/",
|
||||||
|
allowInsecure: false,
|
||||||
|
target: "https://issuer.com",
|
||||||
|
},
|
||||||
|
res{
|
||||||
|
issuer: "https://issuer.com/custom/",
|
||||||
|
err: nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"custom path no leading slash",
|
||||||
|
args{
|
||||||
|
path: "custom/",
|
||||||
|
allowInsecure: false,
|
||||||
|
target: "https://issuer.com",
|
||||||
|
},
|
||||||
|
res{
|
||||||
|
issuer: "https://issuer.com/custom/",
|
||||||
|
err: nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"empty path unsecure",
|
||||||
|
args{
|
||||||
|
path: "",
|
||||||
|
allowInsecure: true,
|
||||||
|
target: "http://issuer.com",
|
||||||
|
},
|
||||||
|
res{
|
||||||
|
issuer: "http://issuer.com",
|
||||||
|
err: nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"custom path unsecure",
|
||||||
|
args{
|
||||||
|
path: "/custom/",
|
||||||
|
allowInsecure: true,
|
||||||
|
target: "http://issuer.com",
|
||||||
|
},
|
||||||
|
res{
|
||||||
|
issuer: "http://issuer.com/custom/",
|
||||||
|
err: nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
issuer, err := IssuerFromHost(tt.args.path)(tt.args.allowInsecure)
|
||||||
|
if tt.res.err == nil {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
req := httptest.NewRequest("", tt.args.target, nil)
|
||||||
|
assert.Equal(t, tt.res.issuer, issuer(req))
|
||||||
|
}
|
||||||
|
if tt.res.err != nil {
|
||||||
|
assert.ErrorIs(t, err, tt.res.err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStaticIssuer(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
issuer string
|
||||||
|
allowInsecure bool
|
||||||
|
}
|
||||||
|
type res struct {
|
||||||
|
issuer string
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
res res
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"invalid issuer",
|
||||||
|
args{
|
||||||
|
issuer: "",
|
||||||
|
allowInsecure: false,
|
||||||
|
},
|
||||||
|
res{
|
||||||
|
issuer: "",
|
||||||
|
err: ErrInvalidIssuerNoIssuer,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"empty path secure",
|
||||||
|
args{
|
||||||
|
issuer: "https://issuer.com",
|
||||||
|
allowInsecure: false,
|
||||||
|
},
|
||||||
|
res{
|
||||||
|
issuer: "https://issuer.com",
|
||||||
|
err: nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"custom path secure",
|
||||||
|
args{
|
||||||
|
issuer: "https://issuer.com/custom/",
|
||||||
|
allowInsecure: false,
|
||||||
|
},
|
||||||
|
res{
|
||||||
|
issuer: "https://issuer.com/custom/",
|
||||||
|
err: nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unsecure",
|
||||||
|
args{
|
||||||
|
issuer: "http://issuer.com",
|
||||||
|
allowInsecure: true,
|
||||||
|
},
|
||||||
|
res{
|
||||||
|
issuer: "http://issuer.com",
|
||||||
|
err: nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
issuer, err := StaticIssuer(tt.args.issuer)(tt.args.allowInsecure)
|
||||||
|
if tt.res.err == nil {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, tt.res.issuer, issuer(nil))
|
||||||
|
}
|
||||||
|
if tt.res.err != nil {
|
||||||
|
assert.ErrorIs(t, err, tt.res.err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
53
pkg/op/context.go
Normal file
53
pkg/op/context.go
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
package op
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type key int
|
||||||
|
|
||||||
|
const (
|
||||||
|
issuerKey key = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
type IssuerInterceptor struct {
|
||||||
|
issuerFromRequest IssuerFromRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewIssuerInterceptor will set the issuer into the context
|
||||||
|
// by the provided IssuerFromRequest (e.g. returned from StaticIssuer or IssuerFromHost)
|
||||||
|
func NewIssuerInterceptor(issuerFromRequest IssuerFromRequest) *IssuerInterceptor {
|
||||||
|
return &IssuerInterceptor{
|
||||||
|
issuerFromRequest: issuerFromRequest,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *IssuerInterceptor) Handler(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
i.setIssuerCtx(w, r, next)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *IssuerInterceptor) HandlerFunc(next http.HandlerFunc) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
i.setIssuerCtx(w, r, next)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IssuerFromContext reads the issuer from the context (set by an IssuerInterceptor)
|
||||||
|
// it will return an empty string if not found
|
||||||
|
func IssuerFromContext(ctx context.Context) string {
|
||||||
|
ctxIssuer, _ := ctx.Value(issuerKey).(string)
|
||||||
|
return ctxIssuer
|
||||||
|
}
|
||||||
|
|
||||||
|
// ContextWithIssuer returns a new context with issuer set to it.
|
||||||
|
func ContextWithIssuer(ctx context.Context, issuer string) context.Context {
|
||||||
|
return context.WithValue(ctx, issuerKey, issuer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *IssuerInterceptor) setIssuerCtx(w http.ResponseWriter, r *http.Request, next http.Handler) {
|
||||||
|
r = r.WithContext(ContextWithIssuer(r.Context(), i.issuerFromRequest(r)))
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
}
|
76
pkg/op/context_test.go
Normal file
76
pkg/op/context_test.go
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
package op
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestIssuerInterceptor(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
issuerFromRequest IssuerFromRequest
|
||||||
|
}
|
||||||
|
type args struct {
|
||||||
|
r *http.Request
|
||||||
|
next http.Handler
|
||||||
|
}
|
||||||
|
type res struct {
|
||||||
|
issuer string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
args args
|
||||||
|
res res
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"empty",
|
||||||
|
fields{
|
||||||
|
func(r *http.Request) string {
|
||||||
|
return ""
|
||||||
|
},
|
||||||
|
},
|
||||||
|
args{},
|
||||||
|
res{
|
||||||
|
issuer: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"static",
|
||||||
|
fields{
|
||||||
|
func(r *http.Request) string {
|
||||||
|
return "static"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
args{},
|
||||||
|
res{
|
||||||
|
issuer: "static",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"host",
|
||||||
|
fields{
|
||||||
|
func(r *http.Request) string {
|
||||||
|
return r.Host
|
||||||
|
},
|
||||||
|
},
|
||||||
|
args{},
|
||||||
|
res{
|
||||||
|
issuer: "issuer.com",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
i := NewIssuerInterceptor(tt.fields.issuerFromRequest)
|
||||||
|
next := http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) {
|
||||||
|
assert.Equal(t, tt.res.issuer, IssuerFromContext(r.Context()))
|
||||||
|
})
|
||||||
|
req := httptest.NewRequest("", "https://issuer.com", nil)
|
||||||
|
i.Handler(next).ServeHTTP(nil, req)
|
||||||
|
i.HandlerFunc(next).ServeHTTP(nil, req)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
package op
|
package op
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/zitadel/oidc/pkg/crypto"
|
"github.com/zitadel/oidc/v2/pkg/crypto"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Crypto interface {
|
type Crypto interface {
|
||||||
|
|
265
pkg/op/device.go
Normal file
265
pkg/op/device.go
Normal file
|
@ -0,0 +1,265 @@
|
||||||
|
package op
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"math/big"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
httphelper "github.com/zitadel/oidc/v2/pkg/http"
|
||||||
|
"github.com/zitadel/oidc/v2/pkg/oidc"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DeviceAuthorizationConfig struct {
|
||||||
|
Lifetime time.Duration
|
||||||
|
PollInterval time.Duration
|
||||||
|
UserFormURL string // the URL where the user must go to authorize the device
|
||||||
|
UserCode UserCodeConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserCodeConfig struct {
|
||||||
|
CharSet string
|
||||||
|
CharAmount int
|
||||||
|
DashInterval int
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
CharSetBase20 = "BCDFGHJKLMNPQRSTVWXZ"
|
||||||
|
CharSetDigits = "0123456789"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
UserCodeBase20 = UserCodeConfig{
|
||||||
|
CharSet: CharSetBase20,
|
||||||
|
CharAmount: 8,
|
||||||
|
DashInterval: 4,
|
||||||
|
}
|
||||||
|
UserCodeDigits = UserCodeConfig{
|
||||||
|
CharSet: CharSetDigits,
|
||||||
|
CharAmount: 9,
|
||||||
|
DashInterval: 3,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func DeviceAuthorizationHandler(o OpenIDProvider) func(http.ResponseWriter, *http.Request) {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if err := DeviceAuthorization(w, r, o); err != nil {
|
||||||
|
RequestError(w, r, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeviceAuthorization(w http.ResponseWriter, r *http.Request, o OpenIDProvider) error {
|
||||||
|
storage, err := assertDeviceStorage(o.Storage())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := ParseDeviceCodeRequest(r, o)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
config := o.DeviceAuthorization()
|
||||||
|
|
||||||
|
deviceCode, err := NewDeviceCode(RecommendedDeviceCodeBytes)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
userCode, err := NewUserCode([]rune(config.UserCode.CharSet), config.UserCode.CharAmount, config.UserCode.DashInterval)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
expires := time.Now().Add(config.Lifetime)
|
||||||
|
err = storage.StoreDeviceAuthorization(r.Context(), req.ClientID, deviceCode, userCode, expires, req.Scopes)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
response := &oidc.DeviceAuthorizationResponse{
|
||||||
|
DeviceCode: deviceCode,
|
||||||
|
UserCode: userCode,
|
||||||
|
VerificationURI: config.UserFormURL,
|
||||||
|
ExpiresIn: int(config.Lifetime / time.Second),
|
||||||
|
Interval: int(config.PollInterval / time.Second),
|
||||||
|
}
|
||||||
|
|
||||||
|
response.VerificationURIComplete = fmt.Sprintf("%s?user_code=%s", config.UserFormURL, userCode)
|
||||||
|
|
||||||
|
httphelper.MarshalJSON(w, response)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseDeviceCodeRequest(r *http.Request, o OpenIDProvider) (*oidc.DeviceAuthorizationRequest, error) {
|
||||||
|
clientID, _, err := ClientIDFromRequest(r, o)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req := new(oidc.DeviceAuthorizationRequest)
|
||||||
|
if err := o.Decoder().Decode(req, r.Form); err != nil {
|
||||||
|
return nil, oidc.ErrInvalidRequest().WithDescription("cannot parse device authentication request").WithParent(err)
|
||||||
|
}
|
||||||
|
req.ClientID = clientID
|
||||||
|
|
||||||
|
return req, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 16 bytes gives 128 bit of entropy.
|
||||||
|
// results in a 22 character base64 encoded string.
|
||||||
|
const RecommendedDeviceCodeBytes = 16
|
||||||
|
|
||||||
|
func NewDeviceCode(nBytes int) (string, error) {
|
||||||
|
bytes := make([]byte, nBytes)
|
||||||
|
if _, err := rand.Read(bytes); err != nil {
|
||||||
|
return "", fmt.Errorf("%w getting entropy for device code", err)
|
||||||
|
}
|
||||||
|
return base64.RawURLEncoding.EncodeToString(bytes), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUserCode(charSet []rune, charAmount, dashInterval int) (string, error) {
|
||||||
|
var buf strings.Builder
|
||||||
|
if dashInterval > 0 {
|
||||||
|
buf.Grow(charAmount + charAmount/dashInterval - 1)
|
||||||
|
} else {
|
||||||
|
buf.Grow(charAmount)
|
||||||
|
}
|
||||||
|
|
||||||
|
max := big.NewInt(int64(len(charSet)))
|
||||||
|
|
||||||
|
for i := 0; i < charAmount; i++ {
|
||||||
|
if dashInterval != 0 && i != 0 && i%dashInterval == 0 {
|
||||||
|
buf.WriteByte('-')
|
||||||
|
}
|
||||||
|
|
||||||
|
bi, err := rand.Int(rand.Reader, max)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("%w getting entropy for user code", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.WriteRune(charSet[int(bi.Int64())])
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type deviceAccessTokenRequest struct {
|
||||||
|
subject string
|
||||||
|
audience []string
|
||||||
|
scopes []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *deviceAccessTokenRequest) GetSubject() string {
|
||||||
|
return r.subject
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *deviceAccessTokenRequest) GetAudience() []string {
|
||||||
|
return r.audience
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *deviceAccessTokenRequest) GetScopes() []string {
|
||||||
|
return r.scopes
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeviceAccessToken(w http.ResponseWriter, r *http.Request, exchanger Exchanger) {
|
||||||
|
if err := deviceAccessToken(w, r, exchanger); err != nil {
|
||||||
|
RequestError(w, r, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func deviceAccessToken(w http.ResponseWriter, r *http.Request, exchanger Exchanger) error {
|
||||||
|
// use a limited context timeout shorter as the default
|
||||||
|
// poll interval of 5 seconds.
|
||||||
|
ctx, cancel := context.WithTimeout(r.Context(), 4*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
r = r.WithContext(ctx)
|
||||||
|
|
||||||
|
clientID, clientAuthenticated, err := ClientIDFromRequest(r, exchanger)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := ParseDeviceAccessTokenRequest(r, exchanger)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
state, err := CheckDeviceAuthorizationState(ctx, clientID, req.DeviceCode, exchanger)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := exchanger.Storage().GetClientByClientID(ctx, clientID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if clientAuthenticated != IsConfidentialType(client) {
|
||||||
|
return oidc.ErrInvalidClient().WithParent(ErrNoClientCredentials).
|
||||||
|
WithDescription("confidential client requires authentication")
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenRequest := &deviceAccessTokenRequest{
|
||||||
|
subject: state.Subject,
|
||||||
|
audience: []string{clientID},
|
||||||
|
scopes: state.Scopes,
|
||||||
|
}
|
||||||
|
resp, err := CreateDeviceTokenResponse(r.Context(), tokenRequest, exchanger, client)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
httphelper.MarshalJSON(w, resp)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseDeviceAccessTokenRequest(r *http.Request, exchanger Exchanger) (*oidc.DeviceAccessTokenRequest, error) {
|
||||||
|
req := new(oidc.DeviceAccessTokenRequest)
|
||||||
|
if err := exchanger.Decoder().Decode(req, r.PostForm); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return req, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func CheckDeviceAuthorizationState(ctx context.Context, clientID, deviceCode string, exchanger Exchanger) (*DeviceAuthorizationState, error) {
|
||||||
|
storage, err := assertDeviceStorage(exchanger.Storage())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
state, err := storage.GetDeviceAuthorizatonState(ctx, clientID, deviceCode)
|
||||||
|
if errors.Is(err, context.DeadlineExceeded) {
|
||||||
|
return nil, oidc.ErrSlowDown().WithParent(err)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, oidc.ErrAccessDenied().WithParent(err)
|
||||||
|
}
|
||||||
|
if state.Denied {
|
||||||
|
return state, oidc.ErrAccessDenied()
|
||||||
|
}
|
||||||
|
if state.Done {
|
||||||
|
return state, nil
|
||||||
|
}
|
||||||
|
if time.Now().After(state.Expires) {
|
||||||
|
return state, oidc.ErrExpiredDeviceCode()
|
||||||
|
}
|
||||||
|
return state, oidc.ErrAuthorizationPending()
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateDeviceTokenResponse(ctx context.Context, tokenRequest TokenRequest, creator TokenCreator, client AccessTokenClient) (*oidc.AccessTokenResponse, error) {
|
||||||
|
accessToken, refreshToken, validity, err := CreateAccessToken(ctx, tokenRequest, AccessTokenTypeBearer, creator, client, "")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &oidc.AccessTokenResponse{
|
||||||
|
AccessToken: accessToken,
|
||||||
|
RefreshToken: refreshToken,
|
||||||
|
TokenType: oidc.BearerToken,
|
||||||
|
ExpiresIn: uint64(validity.Seconds()),
|
||||||
|
}, nil
|
||||||
|
}
|
407
pkg/op/device_test.go
Normal file
407
pkg/op/device_test.go
Normal file
|
@ -0,0 +1,407 @@
|
||||||
|
package op_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
|
"io"
|
||||||
|
mr "math/rand"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/zitadel/oidc/v2/pkg/oidc"
|
||||||
|
"github.com/zitadel/oidc/v2/pkg/op"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_deviceAuthorizationHandler(t *testing.T) {
|
||||||
|
req := &oidc.DeviceAuthorizationRequest{
|
||||||
|
Scopes: []string{"foo", "bar"},
|
||||||
|
ClientID: "web",
|
||||||
|
}
|
||||||
|
values := make(url.Values)
|
||||||
|
testProvider.Encoder().Encode(req, values)
|
||||||
|
body := strings.NewReader(values.Encode())
|
||||||
|
|
||||||
|
r := httptest.NewRequest(http.MethodPost, "/", body)
|
||||||
|
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
runWithRandReader(mr.New(mr.NewSource(1)), func() {
|
||||||
|
op.DeviceAuthorizationHandler(testProvider)(w, r)
|
||||||
|
})
|
||||||
|
|
||||||
|
result := w.Result()
|
||||||
|
|
||||||
|
assert.Less(t, result.StatusCode, 300)
|
||||||
|
|
||||||
|
got, _ := io.ReadAll(result.Body)
|
||||||
|
assert.JSONEq(t, `{"device_code":"Uv38ByGCZU8WP18PmmIdcg", "expires_in":300, "interval":5, "user_code":"JKRV-FRGK", "verification_uri":"https://localhost:9998/device", "verification_uri_complete":"https://localhost:9998/device?user_code=JKRV-FRGK"}`, string(got))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseDeviceCodeRequest(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
req *oidc.DeviceAuthorizationRequest
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "empty request",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "success",
|
||||||
|
req: &oidc.DeviceAuthorizationRequest{
|
||||||
|
Scopes: oidc.SpaceDelimitedArray{"foo", "bar"},
|
||||||
|
ClientID: "web",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
var body io.Reader
|
||||||
|
if tt.req != nil {
|
||||||
|
values := make(url.Values)
|
||||||
|
testProvider.Encoder().Encode(tt.req, values)
|
||||||
|
body = strings.NewReader(values.Encode())
|
||||||
|
}
|
||||||
|
|
||||||
|
r := httptest.NewRequest(http.MethodPost, "/", body)
|
||||||
|
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
|
||||||
|
got, err := op.ParseDeviceCodeRequest(r, testProvider)
|
||||||
|
if tt.wantErr {
|
||||||
|
require.Error(t, err)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
assert.Equal(t, tt.req, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runWithRandReader(r io.Reader, f func()) {
|
||||||
|
originalReader := rand.Reader
|
||||||
|
rand.Reader = r
|
||||||
|
defer func() {
|
||||||
|
rand.Reader = originalReader
|
||||||
|
}()
|
||||||
|
|
||||||
|
f()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewDeviceCode(t *testing.T) {
|
||||||
|
t.Run("reader error", func(t *testing.T) {
|
||||||
|
runWithRandReader(errReader{}, func() {
|
||||||
|
_, err := op.NewDeviceCode(16)
|
||||||
|
require.Error(t, err)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("different lengths, rand reader", func(t *testing.T) {
|
||||||
|
for i := 1; i <= 32; i++ {
|
||||||
|
got, err := op.NewDeviceCode(i)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Len(t, got, base64.RawURLEncoding.EncodedLen(i))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewUserCode(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
charset []rune
|
||||||
|
charAmount int
|
||||||
|
dashInterval int
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
reader io.Reader
|
||||||
|
want string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "reader error",
|
||||||
|
args: args{
|
||||||
|
charset: []rune(op.CharSetBase20),
|
||||||
|
charAmount: 8,
|
||||||
|
dashInterval: 4,
|
||||||
|
},
|
||||||
|
reader: errReader{},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "base20",
|
||||||
|
args: args{
|
||||||
|
charset: []rune(op.CharSetBase20),
|
||||||
|
charAmount: 8,
|
||||||
|
dashInterval: 4,
|
||||||
|
},
|
||||||
|
reader: mr.New(mr.NewSource(1)),
|
||||||
|
want: "XKCD-HTTD",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "digits",
|
||||||
|
args: args{
|
||||||
|
charset: []rune(op.CharSetDigits),
|
||||||
|
charAmount: 9,
|
||||||
|
dashInterval: 3,
|
||||||
|
},
|
||||||
|
reader: mr.New(mr.NewSource(1)),
|
||||||
|
want: "271-256-225",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no dashes",
|
||||||
|
args: args{
|
||||||
|
charset: []rune(op.CharSetDigits),
|
||||||
|
charAmount: 9,
|
||||||
|
},
|
||||||
|
reader: mr.New(mr.NewSource(1)),
|
||||||
|
want: "271256225",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
runWithRandReader(tt.reader, func() {
|
||||||
|
got, err := op.NewUserCode(tt.args.charset, tt.args.charAmount, tt.args.dashInterval)
|
||||||
|
if tt.wantErr {
|
||||||
|
require.ErrorIs(t, err, io.ErrNoProgress)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
assert.Equal(t, tt.want, got)
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("crypto/rand", func(t *testing.T) {
|
||||||
|
const testN = 100000
|
||||||
|
|
||||||
|
for _, c := range []op.UserCodeConfig{op.UserCodeBase20, op.UserCodeDigits} {
|
||||||
|
t.Run(c.CharSet, func(t *testing.T) {
|
||||||
|
results := make(map[string]int)
|
||||||
|
|
||||||
|
for i := 0; i < testN; i++ {
|
||||||
|
code, err := op.NewUserCode([]rune(c.CharSet), c.CharAmount, c.DashInterval)
|
||||||
|
require.NoError(t, err)
|
||||||
|
results[code]++
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Log(results)
|
||||||
|
|
||||||
|
var duplicates int
|
||||||
|
for code, count := range results {
|
||||||
|
assert.Less(t, count, 3, code)
|
||||||
|
if count == 2 {
|
||||||
|
duplicates++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkNewUserCode(b *testing.B) {
|
||||||
|
type args struct {
|
||||||
|
charset []rune
|
||||||
|
charAmount int
|
||||||
|
dashInterval int
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
reader io.Reader
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "math rand, base20",
|
||||||
|
args: args{
|
||||||
|
charset: []rune(op.CharSetBase20),
|
||||||
|
charAmount: 8,
|
||||||
|
dashInterval: 4,
|
||||||
|
},
|
||||||
|
reader: mr.New(mr.NewSource(1)),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "math rand, digits",
|
||||||
|
args: args{
|
||||||
|
charset: []rune(op.CharSetDigits),
|
||||||
|
charAmount: 9,
|
||||||
|
dashInterval: 3,
|
||||||
|
},
|
||||||
|
reader: mr.New(mr.NewSource(1)),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "crypto rand, base20",
|
||||||
|
args: args{
|
||||||
|
charset: []rune(op.CharSetBase20),
|
||||||
|
charAmount: 8,
|
||||||
|
dashInterval: 4,
|
||||||
|
},
|
||||||
|
reader: rand.Reader,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "crypto rand, digits",
|
||||||
|
args: args{
|
||||||
|
charset: []rune(op.CharSetDigits),
|
||||||
|
charAmount: 9,
|
||||||
|
dashInterval: 3,
|
||||||
|
},
|
||||||
|
reader: rand.Reader,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
runWithRandReader(tt.reader, func() {
|
||||||
|
b.Run(tt.name, func(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
_, err := op.NewUserCode(tt.args.charset, tt.args.charAmount, tt.args.dashInterval)
|
||||||
|
require.NoError(b, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeviceAccessToken(t *testing.T) {
|
||||||
|
storage := testProvider.Storage().(op.DeviceAuthorizationStorage)
|
||||||
|
storage.StoreDeviceAuthorization(context.Background(), "native", "qwerty", "yuiop", time.Now().Add(time.Minute), []string{"foo"})
|
||||||
|
storage.CompleteDeviceAuthorization(context.Background(), "yuiop", "tim")
|
||||||
|
|
||||||
|
values := make(url.Values)
|
||||||
|
values.Set("client_id", "native")
|
||||||
|
values.Set("grant_type", string(oidc.GrantTypeDeviceCode))
|
||||||
|
values.Set("device_code", "qwerty")
|
||||||
|
|
||||||
|
r := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(values.Encode()))
|
||||||
|
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
op.DeviceAccessToken(w, r, testProvider)
|
||||||
|
|
||||||
|
result := w.Result()
|
||||||
|
got, _ := io.ReadAll(result.Body)
|
||||||
|
t.Log(string(got))
|
||||||
|
assert.Less(t, result.StatusCode, 300)
|
||||||
|
assert.NotEmpty(t, string(got))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCheckDeviceAuthorizationState(t *testing.T) {
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
storage := testProvider.Storage().(op.DeviceAuthorizationStorage)
|
||||||
|
storage.StoreDeviceAuthorization(context.Background(), "native", "pending", "pending", now.Add(time.Minute), []string{"foo"})
|
||||||
|
storage.StoreDeviceAuthorization(context.Background(), "native", "denied", "denied", now.Add(time.Minute), []string{"foo"})
|
||||||
|
storage.StoreDeviceAuthorization(context.Background(), "native", "completed", "completed", now.Add(time.Minute), []string{"foo"})
|
||||||
|
storage.StoreDeviceAuthorization(context.Background(), "native", "expired", "expired", now.Add(-time.Minute), []string{"foo"})
|
||||||
|
|
||||||
|
storage.DenyDeviceAuthorization(context.Background(), "denied")
|
||||||
|
storage.CompleteDeviceAuthorization(context.Background(), "completed", "tim")
|
||||||
|
|
||||||
|
exceededCtx, cancel := context.WithTimeout(context.Background(), -time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
type args struct {
|
||||||
|
ctx context.Context
|
||||||
|
clientID string
|
||||||
|
deviceCode string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
want *op.DeviceAuthorizationState
|
||||||
|
wantErr error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "pending",
|
||||||
|
args: args{
|
||||||
|
ctx: context.Background(),
|
||||||
|
clientID: "native",
|
||||||
|
deviceCode: "pending",
|
||||||
|
},
|
||||||
|
want: &op.DeviceAuthorizationState{
|
||||||
|
ClientID: "native",
|
||||||
|
Scopes: []string{"foo"},
|
||||||
|
Expires: now.Add(time.Minute),
|
||||||
|
},
|
||||||
|
wantErr: oidc.ErrAuthorizationPending(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "slow down",
|
||||||
|
args: args{
|
||||||
|
ctx: exceededCtx,
|
||||||
|
clientID: "native",
|
||||||
|
deviceCode: "ok",
|
||||||
|
},
|
||||||
|
wantErr: oidc.ErrSlowDown(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wrong client",
|
||||||
|
args: args{
|
||||||
|
ctx: context.Background(),
|
||||||
|
clientID: "foo",
|
||||||
|
deviceCode: "ok",
|
||||||
|
},
|
||||||
|
wantErr: oidc.ErrAccessDenied(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "denied",
|
||||||
|
args: args{
|
||||||
|
ctx: context.Background(),
|
||||||
|
clientID: "native",
|
||||||
|
deviceCode: "denied",
|
||||||
|
},
|
||||||
|
want: &op.DeviceAuthorizationState{
|
||||||
|
ClientID: "native",
|
||||||
|
Scopes: []string{"foo"},
|
||||||
|
Expires: now.Add(time.Minute),
|
||||||
|
Denied: true,
|
||||||
|
},
|
||||||
|
wantErr: oidc.ErrAccessDenied(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "completed",
|
||||||
|
args: args{
|
||||||
|
ctx: context.Background(),
|
||||||
|
clientID: "native",
|
||||||
|
deviceCode: "completed",
|
||||||
|
},
|
||||||
|
want: &op.DeviceAuthorizationState{
|
||||||
|
ClientID: "native",
|
||||||
|
Scopes: []string{"foo"},
|
||||||
|
Expires: now.Add(time.Minute),
|
||||||
|
Subject: "tim",
|
||||||
|
Done: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "expired",
|
||||||
|
args: args{
|
||||||
|
ctx: context.Background(),
|
||||||
|
clientID: "native",
|
||||||
|
deviceCode: "expired",
|
||||||
|
},
|
||||||
|
want: &op.DeviceAuthorizationState{
|
||||||
|
ClientID: "native",
|
||||||
|
Scopes: []string{"foo"},
|
||||||
|
Expires: now.Add(-time.Minute),
|
||||||
|
},
|
||||||
|
wantErr: oidc.ErrExpiredDeviceCode(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got, err := op.CheckDeviceAuthorizationState(tt.args.ctx, tt.args.clientID, tt.args.deviceCode, testProvider)
|
||||||
|
require.ErrorIs(t, err, tt.wantErr)
|
||||||
|
assert.Equal(t, tt.want, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,49 +1,17 @@
|
||||||
package op
|
package op
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
httphelper "github.com/zitadel/oidc/pkg/http"
|
"gopkg.in/square/go-jose.v2"
|
||||||
"github.com/zitadel/oidc/pkg/oidc"
|
|
||||||
|
httphelper "github.com/zitadel/oidc/v2/pkg/http"
|
||||||
|
"github.com/zitadel/oidc/v2/pkg/oidc"
|
||||||
)
|
)
|
||||||
|
|
||||||
func discoveryHandler(c Configuration, s Signer) func(http.ResponseWriter, *http.Request) {
|
type DiscoverStorage interface {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
SignatureAlgorithms(context.Context) ([]jose.SignatureAlgorithm, error)
|
||||||
Discover(w, CreateDiscoveryConfig(c, s))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Discover(w http.ResponseWriter, config *oidc.DiscoveryConfiguration) {
|
|
||||||
httphelper.MarshalJSON(w, config)
|
|
||||||
}
|
|
||||||
|
|
||||||
func CreateDiscoveryConfig(c Configuration, s Signer) *oidc.DiscoveryConfiguration {
|
|
||||||
return &oidc.DiscoveryConfiguration{
|
|
||||||
Issuer: c.Issuer(),
|
|
||||||
AuthorizationEndpoint: c.AuthorizationEndpoint().Absolute(c.Issuer()),
|
|
||||||
TokenEndpoint: c.TokenEndpoint().Absolute(c.Issuer()),
|
|
||||||
IntrospectionEndpoint: c.IntrospectionEndpoint().Absolute(c.Issuer()),
|
|
||||||
UserinfoEndpoint: c.UserinfoEndpoint().Absolute(c.Issuer()),
|
|
||||||
RevocationEndpoint: c.RevocationEndpoint().Absolute(c.Issuer()),
|
|
||||||
EndSessionEndpoint: c.EndSessionEndpoint().Absolute(c.Issuer()),
|
|
||||||
JwksURI: c.KeysEndpoint().Absolute(c.Issuer()),
|
|
||||||
ScopesSupported: Scopes(c),
|
|
||||||
ResponseTypesSupported: ResponseTypes(c),
|
|
||||||
GrantTypesSupported: GrantTypes(c),
|
|
||||||
SubjectTypesSupported: SubjectTypes(c),
|
|
||||||
IDTokenSigningAlgValuesSupported: SigAlgorithms(s),
|
|
||||||
RequestObjectSigningAlgValuesSupported: RequestObjectSigAlgorithms(c),
|
|
||||||
TokenEndpointAuthMethodsSupported: AuthMethodsTokenEndpoint(c),
|
|
||||||
TokenEndpointAuthSigningAlgValuesSupported: TokenSigAlgorithms(c),
|
|
||||||
IntrospectionEndpointAuthSigningAlgValuesSupported: IntrospectionSigAlgorithms(c),
|
|
||||||
IntrospectionEndpointAuthMethodsSupported: AuthMethodsIntrospectionEndpoint(c),
|
|
||||||
RevocationEndpointAuthSigningAlgValuesSupported: RevocationSigAlgorithms(c),
|
|
||||||
RevocationEndpointAuthMethodsSupported: AuthMethodsRevocationEndpoint(c),
|
|
||||||
ClaimsSupported: SupportedClaims(c),
|
|
||||||
CodeChallengeMethodsSupported: CodeChallengeMethods(c),
|
|
||||||
UILocalesSupported: c.SupportedUILocales(),
|
|
||||||
RequestParameterSupported: c.RequestObjectSupported(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var DefaultSupportedScopes = []string{
|
var DefaultSupportedScopes = []string{
|
||||||
|
@ -55,6 +23,47 @@ var DefaultSupportedScopes = []string{
|
||||||
oidc.ScopeOfflineAccess,
|
oidc.ScopeOfflineAccess,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func discoveryHandler(c Configuration, s DiscoverStorage) func(http.ResponseWriter, *http.Request) {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
Discover(w, CreateDiscoveryConfig(r, c, s))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Discover(w http.ResponseWriter, config *oidc.DiscoveryConfiguration) {
|
||||||
|
httphelper.MarshalJSON(w, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateDiscoveryConfig(r *http.Request, config Configuration, storage DiscoverStorage) *oidc.DiscoveryConfiguration {
|
||||||
|
issuer := config.IssuerFromRequest(r)
|
||||||
|
return &oidc.DiscoveryConfiguration{
|
||||||
|
Issuer: issuer,
|
||||||
|
AuthorizationEndpoint: config.AuthorizationEndpoint().Absolute(issuer),
|
||||||
|
TokenEndpoint: config.TokenEndpoint().Absolute(issuer),
|
||||||
|
IntrospectionEndpoint: config.IntrospectionEndpoint().Absolute(issuer),
|
||||||
|
UserinfoEndpoint: config.UserinfoEndpoint().Absolute(issuer),
|
||||||
|
RevocationEndpoint: config.RevocationEndpoint().Absolute(issuer),
|
||||||
|
EndSessionEndpoint: config.EndSessionEndpoint().Absolute(issuer),
|
||||||
|
JwksURI: config.KeysEndpoint().Absolute(issuer),
|
||||||
|
DeviceAuthorizationEndpoint: config.DeviceAuthorizationEndpoint().Absolute(issuer),
|
||||||
|
ScopesSupported: Scopes(config),
|
||||||
|
ResponseTypesSupported: ResponseTypes(config),
|
||||||
|
GrantTypesSupported: GrantTypes(config),
|
||||||
|
SubjectTypesSupported: SubjectTypes(config),
|
||||||
|
IDTokenSigningAlgValuesSupported: SigAlgorithms(r.Context(), storage),
|
||||||
|
RequestObjectSigningAlgValuesSupported: RequestObjectSigAlgorithms(config),
|
||||||
|
TokenEndpointAuthMethodsSupported: AuthMethodsTokenEndpoint(config),
|
||||||
|
TokenEndpointAuthSigningAlgValuesSupported: TokenSigAlgorithms(config),
|
||||||
|
IntrospectionEndpointAuthSigningAlgValuesSupported: IntrospectionSigAlgorithms(config),
|
||||||
|
IntrospectionEndpointAuthMethodsSupported: AuthMethodsIntrospectionEndpoint(config),
|
||||||
|
RevocationEndpointAuthSigningAlgValuesSupported: RevocationSigAlgorithms(config),
|
||||||
|
RevocationEndpointAuthMethodsSupported: AuthMethodsRevocationEndpoint(config),
|
||||||
|
ClaimsSupported: SupportedClaims(config),
|
||||||
|
CodeChallengeMethodsSupported: CodeChallengeMethods(config),
|
||||||
|
UILocalesSupported: config.SupportedUILocales(),
|
||||||
|
RequestParameterSupported: config.RequestObjectSupported(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func Scopes(c Configuration) []string {
|
func Scopes(c Configuration) []string {
|
||||||
return DefaultSupportedScopes // TODO: config
|
return DefaultSupportedScopes // TODO: config
|
||||||
}
|
}
|
||||||
|
@ -84,9 +93,94 @@ func GrantTypes(c Configuration) []oidc.GrantType {
|
||||||
if c.GrantTypeJWTAuthorizationSupported() {
|
if c.GrantTypeJWTAuthorizationSupported() {
|
||||||
grantTypes = append(grantTypes, oidc.GrantTypeBearer)
|
grantTypes = append(grantTypes, oidc.GrantTypeBearer)
|
||||||
}
|
}
|
||||||
|
if c.GrantTypeDeviceCodeSupported() {
|
||||||
|
grantTypes = append(grantTypes, oidc.GrantTypeDeviceCode)
|
||||||
|
}
|
||||||
return grantTypes
|
return grantTypes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func SubjectTypes(c Configuration) []string {
|
||||||
|
return []string{"public"} //TODO: config
|
||||||
|
}
|
||||||
|
|
||||||
|
func SigAlgorithms(ctx context.Context, storage DiscoverStorage) []string {
|
||||||
|
algorithms, err := storage.SignatureAlgorithms(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
algs := make([]string, len(algorithms))
|
||||||
|
for i, algorithm := range algorithms {
|
||||||
|
algs[i] = string(algorithm)
|
||||||
|
}
|
||||||
|
return algs
|
||||||
|
}
|
||||||
|
|
||||||
|
func RequestObjectSigAlgorithms(c Configuration) []string {
|
||||||
|
if !c.RequestObjectSupported() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return c.RequestObjectSigningAlgorithmsSupported()
|
||||||
|
}
|
||||||
|
|
||||||
|
func AuthMethodsTokenEndpoint(c Configuration) []oidc.AuthMethod {
|
||||||
|
authMethods := []oidc.AuthMethod{
|
||||||
|
oidc.AuthMethodNone,
|
||||||
|
oidc.AuthMethodBasic,
|
||||||
|
}
|
||||||
|
if c.AuthMethodPostSupported() {
|
||||||
|
authMethods = append(authMethods, oidc.AuthMethodPost)
|
||||||
|
}
|
||||||
|
if c.AuthMethodPrivateKeyJWTSupported() {
|
||||||
|
authMethods = append(authMethods, oidc.AuthMethodPrivateKeyJWT)
|
||||||
|
}
|
||||||
|
return authMethods
|
||||||
|
}
|
||||||
|
|
||||||
|
func TokenSigAlgorithms(c Configuration) []string {
|
||||||
|
if !c.AuthMethodPrivateKeyJWTSupported() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return c.TokenEndpointSigningAlgorithmsSupported()
|
||||||
|
}
|
||||||
|
|
||||||
|
func IntrospectionSigAlgorithms(c Configuration) []string {
|
||||||
|
if !c.IntrospectionAuthMethodPrivateKeyJWTSupported() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return c.IntrospectionEndpointSigningAlgorithmsSupported()
|
||||||
|
}
|
||||||
|
|
||||||
|
func AuthMethodsIntrospectionEndpoint(c Configuration) []oidc.AuthMethod {
|
||||||
|
authMethods := []oidc.AuthMethod{
|
||||||
|
oidc.AuthMethodBasic,
|
||||||
|
}
|
||||||
|
if c.AuthMethodPrivateKeyJWTSupported() {
|
||||||
|
authMethods = append(authMethods, oidc.AuthMethodPrivateKeyJWT)
|
||||||
|
}
|
||||||
|
return authMethods
|
||||||
|
}
|
||||||
|
|
||||||
|
func RevocationSigAlgorithms(c Configuration) []string {
|
||||||
|
if !c.RevocationAuthMethodPrivateKeyJWTSupported() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return c.RevocationEndpointSigningAlgorithmsSupported()
|
||||||
|
}
|
||||||
|
|
||||||
|
func AuthMethodsRevocationEndpoint(c Configuration) []oidc.AuthMethod {
|
||||||
|
authMethods := []oidc.AuthMethod{
|
||||||
|
oidc.AuthMethodNone,
|
||||||
|
oidc.AuthMethodBasic,
|
||||||
|
}
|
||||||
|
if c.AuthMethodPostSupported() {
|
||||||
|
authMethods = append(authMethods, oidc.AuthMethodPost)
|
||||||
|
}
|
||||||
|
if c.AuthMethodPrivateKeyJWTSupported() {
|
||||||
|
authMethods = append(authMethods, oidc.AuthMethodPrivateKeyJWT)
|
||||||
|
}
|
||||||
|
return authMethods
|
||||||
|
}
|
||||||
|
|
||||||
func SupportedClaims(c Configuration) []string {
|
func SupportedClaims(c Configuration) []string {
|
||||||
return []string{ // TODO: config
|
return []string{ // TODO: config
|
||||||
"sub",
|
"sub",
|
||||||
|
@ -116,59 +210,6 @@ func SupportedClaims(c Configuration) []string {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func SigAlgorithms(s Signer) []string {
|
|
||||||
return []string{string(s.SignatureAlgorithm())}
|
|
||||||
}
|
|
||||||
|
|
||||||
func SubjectTypes(c Configuration) []string {
|
|
||||||
return []string{"public"} // TODO: config
|
|
||||||
}
|
|
||||||
|
|
||||||
func AuthMethodsTokenEndpoint(c Configuration) []oidc.AuthMethod {
|
|
||||||
authMethods := []oidc.AuthMethod{
|
|
||||||
oidc.AuthMethodNone,
|
|
||||||
oidc.AuthMethodBasic,
|
|
||||||
}
|
|
||||||
if c.AuthMethodPostSupported() {
|
|
||||||
authMethods = append(authMethods, oidc.AuthMethodPost)
|
|
||||||
}
|
|
||||||
if c.AuthMethodPrivateKeyJWTSupported() {
|
|
||||||
authMethods = append(authMethods, oidc.AuthMethodPrivateKeyJWT)
|
|
||||||
}
|
|
||||||
return authMethods
|
|
||||||
}
|
|
||||||
|
|
||||||
func TokenSigAlgorithms(c Configuration) []string {
|
|
||||||
if !c.AuthMethodPrivateKeyJWTSupported() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return c.TokenEndpointSigningAlgorithmsSupported()
|
|
||||||
}
|
|
||||||
|
|
||||||
func AuthMethodsIntrospectionEndpoint(c Configuration) []oidc.AuthMethod {
|
|
||||||
authMethods := []oidc.AuthMethod{
|
|
||||||
oidc.AuthMethodBasic,
|
|
||||||
}
|
|
||||||
if c.AuthMethodPrivateKeyJWTSupported() {
|
|
||||||
authMethods = append(authMethods, oidc.AuthMethodPrivateKeyJWT)
|
|
||||||
}
|
|
||||||
return authMethods
|
|
||||||
}
|
|
||||||
|
|
||||||
func AuthMethodsRevocationEndpoint(c Configuration) []oidc.AuthMethod {
|
|
||||||
authMethods := []oidc.AuthMethod{
|
|
||||||
oidc.AuthMethodNone,
|
|
||||||
oidc.AuthMethodBasic,
|
|
||||||
}
|
|
||||||
if c.AuthMethodPostSupported() {
|
|
||||||
authMethods = append(authMethods, oidc.AuthMethodPost)
|
|
||||||
}
|
|
||||||
if c.AuthMethodPrivateKeyJWTSupported() {
|
|
||||||
authMethods = append(authMethods, oidc.AuthMethodPrivateKeyJWT)
|
|
||||||
}
|
|
||||||
return authMethods
|
|
||||||
}
|
|
||||||
|
|
||||||
func CodeChallengeMethods(c Configuration) []oidc.CodeChallengeMethod {
|
func CodeChallengeMethods(c Configuration) []oidc.CodeChallengeMethod {
|
||||||
codeMethods := make([]oidc.CodeChallengeMethod, 0, 1)
|
codeMethods := make([]oidc.CodeChallengeMethod, 0, 1)
|
||||||
if c.CodeMethodS256Supported() {
|
if c.CodeMethodS256Supported() {
|
||||||
|
@ -176,24 +217,3 @@ func CodeChallengeMethods(c Configuration) []oidc.CodeChallengeMethod {
|
||||||
}
|
}
|
||||||
return codeMethods
|
return codeMethods
|
||||||
}
|
}
|
||||||
|
|
||||||
func IntrospectionSigAlgorithms(c Configuration) []string {
|
|
||||||
if !c.IntrospectionAuthMethodPrivateKeyJWTSupported() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return c.IntrospectionEndpointSigningAlgorithmsSupported()
|
|
||||||
}
|
|
||||||
|
|
||||||
func RevocationSigAlgorithms(c Configuration) []string {
|
|
||||||
if !c.RevocationAuthMethodPrivateKeyJWTSupported() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return c.RevocationEndpointSigningAlgorithmsSupported()
|
|
||||||
}
|
|
||||||
|
|
||||||
func RequestObjectSigAlgorithms(c Configuration) []string {
|
|
||||||
if !c.RequestObjectSupported() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return c.RequestObjectSigningAlgorithmsSupported()
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,18 +1,19 @@
|
||||||
package op_test
|
package op_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"reflect"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/golang/mock/gomock"
|
"github.com/golang/mock/gomock"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"gopkg.in/square/go-jose.v2"
|
"gopkg.in/square/go-jose.v2"
|
||||||
|
|
||||||
"github.com/zitadel/oidc/pkg/oidc"
|
"github.com/zitadel/oidc/v2/pkg/oidc"
|
||||||
"github.com/zitadel/oidc/pkg/op"
|
"github.com/zitadel/oidc/v2/pkg/op"
|
||||||
"github.com/zitadel/oidc/pkg/op/mock"
|
"github.com/zitadel/oidc/v2/pkg/op/mock"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestDiscover(t *testing.T) {
|
func TestDiscover(t *testing.T) {
|
||||||
|
@ -47,8 +48,9 @@ func TestDiscover(t *testing.T) {
|
||||||
|
|
||||||
func TestCreateDiscoveryConfig(t *testing.T) {
|
func TestCreateDiscoveryConfig(t *testing.T) {
|
||||||
type args struct {
|
type args struct {
|
||||||
|
request *http.Request
|
||||||
c op.Configuration
|
c op.Configuration
|
||||||
s op.Signer
|
s op.DiscoverStorage
|
||||||
}
|
}
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
@ -59,9 +61,8 @@ func TestCreateDiscoveryConfig(t *testing.T) {
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
if got := op.CreateDiscoveryConfig(tt.args.c, tt.args.s); !reflect.DeepEqual(got, tt.want) {
|
got := op.CreateDiscoveryConfig(tt.args.request, tt.args.c, tt.args.s)
|
||||||
t.Errorf("CreateDiscoveryConfig() = %v, want %v", got, tt.want)
|
assert.Equal(t, tt.want, got)
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -83,9 +84,8 @@ func Test_scopes(t *testing.T) {
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
if got := op.Scopes(tt.args.c); !reflect.DeepEqual(got, tt.want) {
|
got := op.Scopes(tt.args.c)
|
||||||
t.Errorf("scopes() = %v, want %v", got, tt.want)
|
assert.Equal(t, tt.want, got)
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -99,13 +99,16 @@ func Test_ResponseTypes(t *testing.T) {
|
||||||
args args
|
args args
|
||||||
want []string
|
want []string
|
||||||
}{
|
}{
|
||||||
// TODO: Add test cases.
|
{
|
||||||
|
"code and implicit flow",
|
||||||
|
args{},
|
||||||
|
[]string{"code", "id_token", "id_token token"},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
if got := op.ResponseTypes(tt.args.c); !reflect.DeepEqual(got, tt.want) {
|
got := op.ResponseTypes(tt.args.c)
|
||||||
t.Errorf("responseTypes() = %v, want %v", got, tt.want)
|
assert.Equal(t, tt.want, got)
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -117,63 +120,53 @@ func Test_GrantTypes(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
args args
|
args args
|
||||||
want []string
|
want []oidc.GrantType
|
||||||
}{
|
|
||||||
// TODO: Add test cases.
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
if got := op.GrantTypes(tt.args.c); !reflect.DeepEqual(got, tt.want) {
|
|
||||||
t.Errorf("grantTypes() = %v, want %v", got, tt.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSupportedClaims(t *testing.T) {
|
|
||||||
type args struct {
|
|
||||||
c op.Configuration
|
|
||||||
}
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
args args
|
|
||||||
want []string
|
|
||||||
}{
|
|
||||||
// TODO: Add test cases.
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
if got := op.SupportedClaims(tt.args.c); !reflect.DeepEqual(got, tt.want) {
|
|
||||||
t.Errorf("SupportedClaims() = %v, want %v", got, tt.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_SigAlgorithms(t *testing.T) {
|
|
||||||
m := mock.NewMockSigner(gomock.NewController(t))
|
|
||||||
type args struct {
|
|
||||||
s op.Signer
|
|
||||||
}
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
args args
|
|
||||||
want []string
|
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
"",
|
"code and implicit flow",
|
||||||
args{func() op.Signer {
|
args{
|
||||||
m.EXPECT().SignatureAlgorithm().Return(jose.RS256)
|
func() op.Configuration {
|
||||||
return m
|
c := mock.NewMockConfiguration(gomock.NewController(t))
|
||||||
}()},
|
c.EXPECT().GrantTypeRefreshTokenSupported().Return(false)
|
||||||
[]string{"RS256"},
|
c.EXPECT().GrantTypeTokenExchangeSupported().Return(false)
|
||||||
|
c.EXPECT().GrantTypeJWTAuthorizationSupported().Return(false)
|
||||||
|
c.EXPECT().GrantTypeClientCredentialsSupported().Return(false)
|
||||||
|
c.EXPECT().GrantTypeDeviceCodeSupported().Return(false)
|
||||||
|
return c
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
[]oidc.GrantType{
|
||||||
|
oidc.GrantTypeCode,
|
||||||
|
oidc.GrantTypeImplicit,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code, implicit flow, refresh token, token exchange, jwt profile, client_credentials",
|
||||||
|
args{
|
||||||
|
func() op.Configuration {
|
||||||
|
c := mock.NewMockConfiguration(gomock.NewController(t))
|
||||||
|
c.EXPECT().GrantTypeRefreshTokenSupported().Return(true)
|
||||||
|
c.EXPECT().GrantTypeTokenExchangeSupported().Return(true)
|
||||||
|
c.EXPECT().GrantTypeJWTAuthorizationSupported().Return(true)
|
||||||
|
c.EXPECT().GrantTypeClientCredentialsSupported().Return(true)
|
||||||
|
c.EXPECT().GrantTypeDeviceCodeSupported().Return(false)
|
||||||
|
return c
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
[]oidc.GrantType{
|
||||||
|
oidc.GrantTypeCode,
|
||||||
|
oidc.GrantTypeImplicit,
|
||||||
|
oidc.GrantTypeRefreshToken,
|
||||||
|
oidc.GrantTypeClientCredentials,
|
||||||
|
oidc.GrantTypeTokenExchange,
|
||||||
|
oidc.GrantTypeBearer,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
if got := op.SigAlgorithms(tt.args.s); !reflect.DeepEqual(got, tt.want) {
|
got := op.GrantTypes(tt.args.c)
|
||||||
t.Errorf("sigAlgorithms() = %v, want %v", got, tt.want)
|
assert.Equal(t, tt.want, got)
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -195,9 +188,80 @@ func Test_SubjectTypes(t *testing.T) {
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
if got := op.SubjectTypes(tt.args.c); !reflect.DeepEqual(got, tt.want) {
|
got := op.SubjectTypes(tt.args.c)
|
||||||
t.Errorf("subjectTypes() = %v, want %v", got, tt.want)
|
assert.Equal(t, tt.want, got)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_SigAlgorithms(t *testing.T) {
|
||||||
|
m := mock.NewMockDiscoverStorage(gomock.NewController(t))
|
||||||
|
type args struct {
|
||||||
|
s op.DiscoverStorage
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
want []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"",
|
||||||
|
args{func() op.DiscoverStorage {
|
||||||
|
m.EXPECT().SignatureAlgorithms(gomock.Any()).Return([]jose.SignatureAlgorithm{jose.RS256}, nil)
|
||||||
|
return m
|
||||||
|
}()},
|
||||||
|
[]string{"RS256"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got := op.SigAlgorithms(context.Background(), tt.args.s)
|
||||||
|
assert.Equal(t, tt.want, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_RequestObjectSigAlgorithms(t *testing.T) {
|
||||||
|
m := mock.NewMockConfiguration(gomock.NewController(t))
|
||||||
|
type args struct {
|
||||||
|
c op.Configuration
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
want []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"not supported, empty",
|
||||||
|
args{func() op.Configuration {
|
||||||
|
m.EXPECT().RequestObjectSupported().Return(false)
|
||||||
|
return m
|
||||||
|
}()},
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"supported, empty",
|
||||||
|
args{func() op.Configuration {
|
||||||
|
m.EXPECT().RequestObjectSupported().Return(true)
|
||||||
|
m.EXPECT().RequestObjectSigningAlgorithmsSupported().Return(nil)
|
||||||
|
return m
|
||||||
|
}()},
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"supported, list",
|
||||||
|
args{func() op.Configuration {
|
||||||
|
m.EXPECT().RequestObjectSupported().Return(true)
|
||||||
|
m.EXPECT().RequestObjectSigningAlgorithmsSupported().Return([]string{"RS256"})
|
||||||
|
return m
|
||||||
|
}()},
|
||||||
|
[]string{"RS256"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got := op.RequestObjectSigAlgorithms(tt.args.c)
|
||||||
|
assert.Equal(t, tt.want, got)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -244,9 +308,311 @@ func Test_AuthMethodsTokenEndpoint(t *testing.T) {
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
if got := op.AuthMethodsTokenEndpoint(tt.args.c); !reflect.DeepEqual(got, tt.want) {
|
got := op.AuthMethodsTokenEndpoint(tt.args.c)
|
||||||
t.Errorf("authMethods() = %v, want %v", got, tt.want)
|
assert.Equal(t, tt.want, got)
|
||||||
}
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_TokenSigAlgorithms(t *testing.T) {
|
||||||
|
m := mock.NewMockConfiguration(gomock.NewController(t))
|
||||||
|
type args struct {
|
||||||
|
c op.Configuration
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
want []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"not supported, empty",
|
||||||
|
args{func() op.Configuration {
|
||||||
|
m.EXPECT().AuthMethodPrivateKeyJWTSupported().Return(false)
|
||||||
|
return m
|
||||||
|
}()},
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"supported, empty",
|
||||||
|
args{func() op.Configuration {
|
||||||
|
m.EXPECT().AuthMethodPrivateKeyJWTSupported().Return(true)
|
||||||
|
m.EXPECT().TokenEndpointSigningAlgorithmsSupported().Return(nil)
|
||||||
|
return m
|
||||||
|
}()},
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"supported, list",
|
||||||
|
args{func() op.Configuration {
|
||||||
|
m.EXPECT().AuthMethodPrivateKeyJWTSupported().Return(true)
|
||||||
|
m.EXPECT().TokenEndpointSigningAlgorithmsSupported().Return([]string{"RS256"})
|
||||||
|
return m
|
||||||
|
}()},
|
||||||
|
[]string{"RS256"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got := op.TokenSigAlgorithms(tt.args.c)
|
||||||
|
assert.Equal(t, tt.want, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_IntrospectionSigAlgorithms(t *testing.T) {
|
||||||
|
m := mock.NewMockConfiguration(gomock.NewController(t))
|
||||||
|
type args struct {
|
||||||
|
c op.Configuration
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
want []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"not supported, empty",
|
||||||
|
args{func() op.Configuration {
|
||||||
|
m.EXPECT().IntrospectionAuthMethodPrivateKeyJWTSupported().Return(false)
|
||||||
|
return m
|
||||||
|
}()},
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"supported, empty",
|
||||||
|
args{func() op.Configuration {
|
||||||
|
m.EXPECT().IntrospectionAuthMethodPrivateKeyJWTSupported().Return(true)
|
||||||
|
m.EXPECT().IntrospectionEndpointSigningAlgorithmsSupported().Return(nil)
|
||||||
|
return m
|
||||||
|
}()},
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"supported, list",
|
||||||
|
args{func() op.Configuration {
|
||||||
|
m.EXPECT().IntrospectionAuthMethodPrivateKeyJWTSupported().Return(true)
|
||||||
|
m.EXPECT().IntrospectionEndpointSigningAlgorithmsSupported().Return([]string{"RS256"})
|
||||||
|
return m
|
||||||
|
}()},
|
||||||
|
[]string{"RS256"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got := op.IntrospectionSigAlgorithms(tt.args.c)
|
||||||
|
assert.Equal(t, tt.want, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_AuthMethodsIntrospectionEndpoint(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
c op.Configuration
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
want []oidc.AuthMethod
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"basic only",
|
||||||
|
args{func() op.Configuration {
|
||||||
|
m := mock.NewMockConfiguration(gomock.NewController(t))
|
||||||
|
m.EXPECT().AuthMethodPrivateKeyJWTSupported().Return(false)
|
||||||
|
return m
|
||||||
|
}()},
|
||||||
|
[]oidc.AuthMethod{oidc.AuthMethodBasic},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"basic and private_key_jwt",
|
||||||
|
args{func() op.Configuration {
|
||||||
|
m := mock.NewMockConfiguration(gomock.NewController(t))
|
||||||
|
m.EXPECT().AuthMethodPrivateKeyJWTSupported().Return(true)
|
||||||
|
return m
|
||||||
|
}()},
|
||||||
|
[]oidc.AuthMethod{oidc.AuthMethodBasic, oidc.AuthMethodPrivateKeyJWT},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got := op.AuthMethodsIntrospectionEndpoint(tt.args.c)
|
||||||
|
assert.Equal(t, tt.want, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_RevocationSigAlgorithms(t *testing.T) {
|
||||||
|
m := mock.NewMockConfiguration(gomock.NewController(t))
|
||||||
|
type args struct {
|
||||||
|
c op.Configuration
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
want []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"not supported, empty",
|
||||||
|
args{func() op.Configuration {
|
||||||
|
m.EXPECT().RevocationAuthMethodPrivateKeyJWTSupported().Return(false)
|
||||||
|
return m
|
||||||
|
}()},
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"supported, empty",
|
||||||
|
args{func() op.Configuration {
|
||||||
|
m.EXPECT().RevocationAuthMethodPrivateKeyJWTSupported().Return(true)
|
||||||
|
m.EXPECT().RevocationEndpointSigningAlgorithmsSupported().Return(nil)
|
||||||
|
return m
|
||||||
|
}()},
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"supported, list",
|
||||||
|
args{func() op.Configuration {
|
||||||
|
m.EXPECT().RevocationAuthMethodPrivateKeyJWTSupported().Return(true)
|
||||||
|
m.EXPECT().RevocationEndpointSigningAlgorithmsSupported().Return([]string{"RS256"})
|
||||||
|
return m
|
||||||
|
}()},
|
||||||
|
[]string{"RS256"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got := op.RevocationSigAlgorithms(tt.args.c)
|
||||||
|
assert.Equal(t, tt.want, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_AuthMethodsRevocationEndpoint(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
c op.Configuration
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
want []oidc.AuthMethod
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"none and basic",
|
||||||
|
args{func() op.Configuration {
|
||||||
|
m := mock.NewMockConfiguration(gomock.NewController(t))
|
||||||
|
m.EXPECT().AuthMethodPostSupported().Return(false)
|
||||||
|
m.EXPECT().AuthMethodPrivateKeyJWTSupported().Return(false)
|
||||||
|
return m
|
||||||
|
}()},
|
||||||
|
[]oidc.AuthMethod{oidc.AuthMethodNone, oidc.AuthMethodBasic},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"none, basic and post",
|
||||||
|
args{func() op.Configuration {
|
||||||
|
m := mock.NewMockConfiguration(gomock.NewController(t))
|
||||||
|
m.EXPECT().AuthMethodPostSupported().Return(true)
|
||||||
|
m.EXPECT().AuthMethodPrivateKeyJWTSupported().Return(false)
|
||||||
|
return m
|
||||||
|
}()},
|
||||||
|
[]oidc.AuthMethod{oidc.AuthMethodNone, oidc.AuthMethodBasic, oidc.AuthMethodPost},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"none, basic, post and private_key_jwt",
|
||||||
|
args{func() op.Configuration {
|
||||||
|
m := mock.NewMockConfiguration(gomock.NewController(t))
|
||||||
|
m.EXPECT().AuthMethodPostSupported().Return(true)
|
||||||
|
m.EXPECT().AuthMethodPrivateKeyJWTSupported().Return(true)
|
||||||
|
return m
|
||||||
|
}()},
|
||||||
|
[]oidc.AuthMethod{oidc.AuthMethodNone, oidc.AuthMethodBasic, oidc.AuthMethodPost, oidc.AuthMethodPrivateKeyJWT},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got := op.AuthMethodsRevocationEndpoint(tt.args.c)
|
||||||
|
assert.Equal(t, tt.want, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSupportedClaims(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
c op.Configuration
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
want []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"scopes",
|
||||||
|
args{},
|
||||||
|
[]string{
|
||||||
|
"sub",
|
||||||
|
"aud",
|
||||||
|
"exp",
|
||||||
|
"iat",
|
||||||
|
"iss",
|
||||||
|
"auth_time",
|
||||||
|
"nonce",
|
||||||
|
"acr",
|
||||||
|
"amr",
|
||||||
|
"c_hash",
|
||||||
|
"at_hash",
|
||||||
|
"act",
|
||||||
|
"scopes",
|
||||||
|
"client_id",
|
||||||
|
"azp",
|
||||||
|
"preferred_username",
|
||||||
|
"name",
|
||||||
|
"family_name",
|
||||||
|
"given_name",
|
||||||
|
"locale",
|
||||||
|
"email",
|
||||||
|
"email_verified",
|
||||||
|
"phone_number",
|
||||||
|
"phone_number_verified",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got := op.SupportedClaims(tt.args.c)
|
||||||
|
assert.Equal(t, tt.want, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_CodeChallengeMethods(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
c op.Configuration
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
want []oidc.CodeChallengeMethod
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"not supported",
|
||||||
|
args{func() op.Configuration {
|
||||||
|
m := mock.NewMockConfiguration(gomock.NewController(t))
|
||||||
|
m.EXPECT().CodeMethodS256Supported().Return(false)
|
||||||
|
return m
|
||||||
|
}()},
|
||||||
|
[]oidc.CodeChallengeMethod{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"S256",
|
||||||
|
args{func() op.Configuration {
|
||||||
|
m := mock.NewMockConfiguration(gomock.NewController(t))
|
||||||
|
m.EXPECT().CodeMethodS256Supported().Return(true)
|
||||||
|
return m
|
||||||
|
}()},
|
||||||
|
[]oidc.CodeChallengeMethod{oidc.CodeChallengeMethodS256},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got := op.CodeChallengeMethods(tt.args.c)
|
||||||
|
assert.Equal(t, tt.want, got)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ package op_test
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/zitadel/oidc/pkg/op"
|
"github.com/zitadel/oidc/v2/pkg/op"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestEndpoint_Path(t *testing.T) {
|
func TestEndpoint_Path(t *testing.T) {
|
||||||
|
|
|
@ -3,8 +3,8 @@ package op
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
httphelper "github.com/zitadel/oidc/pkg/http"
|
httphelper "github.com/zitadel/oidc/v2/pkg/http"
|
||||||
"github.com/zitadel/oidc/pkg/oidc"
|
"github.com/zitadel/oidc/v2/pkg/oidc"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ErrAuthRequest interface {
|
type ErrAuthRequest interface {
|
||||||
|
|
|
@ -6,11 +6,11 @@ import (
|
||||||
|
|
||||||
"gopkg.in/square/go-jose.v2"
|
"gopkg.in/square/go-jose.v2"
|
||||||
|
|
||||||
httphelper "github.com/zitadel/oidc/pkg/http"
|
httphelper "github.com/zitadel/oidc/v2/pkg/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
type KeyProvider interface {
|
type KeyProvider interface {
|
||||||
GetKeySet(context.Context) (*jose.JSONWebKeySet, error)
|
KeySet(context.Context) ([]Key, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func keysHandler(k KeyProvider) func(http.ResponseWriter, *http.Request) {
|
func keysHandler(k KeyProvider) func(http.ResponseWriter, *http.Request) {
|
||||||
|
@ -20,10 +20,23 @@ func keysHandler(k KeyProvider) func(http.ResponseWriter, *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func Keys(w http.ResponseWriter, r *http.Request, k KeyProvider) {
|
func Keys(w http.ResponseWriter, r *http.Request, k KeyProvider) {
|
||||||
keySet, err := k.GetKeySet(r.Context())
|
keySet, err := k.KeySet(r.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
httphelper.MarshalJSONWithStatus(w, err, http.StatusInternalServerError)
|
httphelper.MarshalJSONWithStatus(w, err, http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
httphelper.MarshalJSON(w, keySet)
|
httphelper.MarshalJSON(w, jsonWebKeySet(keySet))
|
||||||
|
}
|
||||||
|
|
||||||
|
func jsonWebKeySet(keys []Key) *jose.JSONWebKeySet {
|
||||||
|
webKeys := make([]jose.JSONWebKey, len(keys))
|
||||||
|
for i, key := range keys {
|
||||||
|
webKeys[i] = jose.JSONWebKey{
|
||||||
|
KeyID: key.ID(),
|
||||||
|
Algorithm: string(key.Algorithm()),
|
||||||
|
Use: key.Use(),
|
||||||
|
Key: key.Key(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &jose.JSONWebKeySet{Keys: webKeys}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,9 +11,9 @@ import (
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"gopkg.in/square/go-jose.v2"
|
"gopkg.in/square/go-jose.v2"
|
||||||
|
|
||||||
"github.com/zitadel/oidc/pkg/oidc"
|
"github.com/zitadel/oidc/v2/pkg/oidc"
|
||||||
"github.com/zitadel/oidc/pkg/op"
|
"github.com/zitadel/oidc/v2/pkg/op"
|
||||||
"github.com/zitadel/oidc/pkg/op/mock"
|
"github.com/zitadel/oidc/v2/pkg/op/mock"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestKeys(t *testing.T) {
|
func TestKeys(t *testing.T) {
|
||||||
|
@ -35,7 +35,7 @@ func TestKeys(t *testing.T) {
|
||||||
args: args{
|
args: args{
|
||||||
k: func() op.KeyProvider {
|
k: func() op.KeyProvider {
|
||||||
m := mock.NewMockKeyProvider(gomock.NewController(t))
|
m := mock.NewMockKeyProvider(gomock.NewController(t))
|
||||||
m.EXPECT().GetKeySet(gomock.Any()).Return(nil, oidc.ErrServerError())
|
m.EXPECT().KeySet(gomock.Any()).Return(nil, oidc.ErrServerError())
|
||||||
return m
|
return m
|
||||||
}(),
|
}(),
|
||||||
},
|
},
|
||||||
|
@ -51,39 +51,39 @@ func TestKeys(t *testing.T) {
|
||||||
args: args{
|
args: args{
|
||||||
k: func() op.KeyProvider {
|
k: func() op.KeyProvider {
|
||||||
m := mock.NewMockKeyProvider(gomock.NewController(t))
|
m := mock.NewMockKeyProvider(gomock.NewController(t))
|
||||||
m.EXPECT().GetKeySet(gomock.Any()).Return(nil, nil)
|
m.EXPECT().KeySet(gomock.Any()).Return(nil, nil)
|
||||||
return m
|
return m
|
||||||
}(),
|
}(),
|
||||||
},
|
},
|
||||||
res: res{
|
res: res{
|
||||||
statusCode: http.StatusOK,
|
statusCode: http.StatusOK,
|
||||||
contentType: "application/json",
|
contentType: "application/json",
|
||||||
|
body: `{"keys":[]}
|
||||||
|
`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "list",
|
name: "list",
|
||||||
args: args{
|
args: args{
|
||||||
k: func() op.KeyProvider {
|
k: func() op.KeyProvider {
|
||||||
m := mock.NewMockKeyProvider(gomock.NewController(t))
|
ctrl := gomock.NewController(t)
|
||||||
m.EXPECT().GetKeySet(gomock.Any()).Return(
|
m := mock.NewMockKeyProvider(ctrl)
|
||||||
&jose.JSONWebKeySet{Keys: []jose.JSONWebKey{
|
k := mock.NewMockKey(ctrl)
|
||||||
{
|
k.EXPECT().Key().Return(&rsa.PublicKey{
|
||||||
Key: &rsa.PublicKey{
|
|
||||||
N: big.NewInt(1),
|
N: big.NewInt(1),
|
||||||
E: 1,
|
E: 1,
|
||||||
},
|
})
|
||||||
KeyID: "id",
|
k.EXPECT().ID().Return("id")
|
||||||
},
|
k.EXPECT().Algorithm().Return(jose.RS256)
|
||||||
}},
|
k.EXPECT().Use().Return("sig")
|
||||||
nil,
|
m.EXPECT().KeySet(gomock.Any()).Return([]op.Key{k}, nil)
|
||||||
)
|
|
||||||
return m
|
return m
|
||||||
}(),
|
}(),
|
||||||
},
|
},
|
||||||
res: res{
|
res: res{
|
||||||
statusCode: http.StatusOK,
|
statusCode: http.StatusOK,
|
||||||
contentType: "application/json",
|
contentType: "application/json",
|
||||||
body: `{"keys":[{"kty":"RSA","kid":"id","n":"AQ","e":"AQ"}]}
|
body: `{"keys":[{"use":"sig","kty":"RSA","kid":"id","alg":"RS256","n":"AQ","e":"AQ"}]}
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,15 +1,16 @@
|
||||||
// Code generated by MockGen. DO NOT EDIT.
|
// Code generated by MockGen. DO NOT EDIT.
|
||||||
// Source: github.com/zitadel/oidc/pkg/op (interfaces: Authorizer)
|
// Source: github.com/zitadel/oidc/v2/pkg/op (interfaces: Authorizer)
|
||||||
|
|
||||||
// Package mock is a generated GoMock package.
|
// Package mock is a generated GoMock package.
|
||||||
package mock
|
package mock
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
context "context"
|
||||||
reflect "reflect"
|
reflect "reflect"
|
||||||
|
|
||||||
gomock "github.com/golang/mock/gomock"
|
gomock "github.com/golang/mock/gomock"
|
||||||
http "github.com/zitadel/oidc/pkg/http"
|
http "github.com/zitadel/oidc/v2/pkg/http"
|
||||||
op "github.com/zitadel/oidc/pkg/op"
|
op "github.com/zitadel/oidc/v2/pkg/op"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MockAuthorizer is a mock of Authorizer interface.
|
// MockAuthorizer is a mock of Authorizer interface.
|
||||||
|
@ -78,31 +79,17 @@ func (mr *MockAuthorizerMockRecorder) Encoder() *gomock.Call {
|
||||||
}
|
}
|
||||||
|
|
||||||
// IDTokenHintVerifier mocks base method.
|
// IDTokenHintVerifier mocks base method.
|
||||||
func (m *MockAuthorizer) IDTokenHintVerifier() op.IDTokenHintVerifier {
|
func (m *MockAuthorizer) IDTokenHintVerifier(arg0 context.Context) op.IDTokenHintVerifier {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
ret := m.ctrl.Call(m, "IDTokenHintVerifier")
|
ret := m.ctrl.Call(m, "IDTokenHintVerifier", arg0)
|
||||||
ret0, _ := ret[0].(op.IDTokenHintVerifier)
|
ret0, _ := ret[0].(op.IDTokenHintVerifier)
|
||||||
return ret0
|
return ret0
|
||||||
}
|
}
|
||||||
|
|
||||||
// IDTokenHintVerifier indicates an expected call of IDTokenHintVerifier.
|
// IDTokenHintVerifier indicates an expected call of IDTokenHintVerifier.
|
||||||
func (mr *MockAuthorizerMockRecorder) IDTokenHintVerifier() *gomock.Call {
|
func (mr *MockAuthorizerMockRecorder) IDTokenHintVerifier(arg0 interface{}) *gomock.Call {
|
||||||
mr.mock.ctrl.T.Helper()
|
mr.mock.ctrl.T.Helper()
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IDTokenHintVerifier", reflect.TypeOf((*MockAuthorizer)(nil).IDTokenHintVerifier))
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IDTokenHintVerifier", reflect.TypeOf((*MockAuthorizer)(nil).IDTokenHintVerifier), arg0)
|
||||||
}
|
|
||||||
|
|
||||||
// Issuer mocks base method.
|
|
||||||
func (m *MockAuthorizer) Issuer() string {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "Issuer")
|
|
||||||
ret0, _ := ret[0].(string)
|
|
||||||
return ret0
|
|
||||||
}
|
|
||||||
|
|
||||||
// Issuer indicates an expected call of Issuer.
|
|
||||||
func (mr *MockAuthorizerMockRecorder) Issuer() *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Issuer", reflect.TypeOf((*MockAuthorizer)(nil).Issuer))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// RequestObjectSupported mocks base method.
|
// RequestObjectSupported mocks base method.
|
||||||
|
@ -119,20 +106,6 @@ func (mr *MockAuthorizerMockRecorder) RequestObjectSupported() *gomock.Call {
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RequestObjectSupported", reflect.TypeOf((*MockAuthorizer)(nil).RequestObjectSupported))
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RequestObjectSupported", reflect.TypeOf((*MockAuthorizer)(nil).RequestObjectSupported))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Signer mocks base method.
|
|
||||||
func (m *MockAuthorizer) Signer() op.Signer {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "Signer")
|
|
||||||
ret0, _ := ret[0].(op.Signer)
|
|
||||||
return ret0
|
|
||||||
}
|
|
||||||
|
|
||||||
// Signer indicates an expected call of Signer.
|
|
||||||
func (mr *MockAuthorizerMockRecorder) Signer() *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Signer", reflect.TypeOf((*MockAuthorizer)(nil).Signer))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Storage mocks base method.
|
// Storage mocks base method.
|
||||||
func (m *MockAuthorizer) Storage() op.Storage {
|
func (m *MockAuthorizer) Storage() op.Storage {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
|
|
|
@ -8,8 +8,8 @@ import (
|
||||||
"github.com/gorilla/schema"
|
"github.com/gorilla/schema"
|
||||||
"gopkg.in/square/go-jose.v2"
|
"gopkg.in/square/go-jose.v2"
|
||||||
|
|
||||||
"github.com/zitadel/oidc/pkg/oidc"
|
"github.com/zitadel/oidc/v2/pkg/oidc"
|
||||||
"github.com/zitadel/oidc/pkg/op"
|
"github.com/zitadel/oidc/v2/pkg/op"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewAuthorizer(t *testing.T) op.Authorizer {
|
func NewAuthorizer(t *testing.T) op.Authorizer {
|
||||||
|
@ -20,23 +20,13 @@ func NewAuthorizerExpectValid(t *testing.T, wantErr bool) op.Authorizer {
|
||||||
m := NewAuthorizer(t)
|
m := NewAuthorizer(t)
|
||||||
ExpectDecoder(m)
|
ExpectDecoder(m)
|
||||||
ExpectEncoder(m)
|
ExpectEncoder(m)
|
||||||
ExpectSigner(m, t)
|
//ExpectSigner(m, t)
|
||||||
ExpectStorage(m, t)
|
ExpectStorage(m, t)
|
||||||
ExpectVerifier(m, t)
|
ExpectVerifier(m, t)
|
||||||
// ExpectErrorHandler(m, t, wantErr)
|
// ExpectErrorHandler(m, t, wantErr)
|
||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
|
|
||||||
// func NewAuthorizerExpectDecoderFails(t *testing.T) op.Authorizer {
|
|
||||||
// m := NewAuthorizer(t)
|
|
||||||
// ExpectDecoderFails(m)
|
|
||||||
// ExpectEncoder(m)
|
|
||||||
// ExpectSigner(m, t)
|
|
||||||
// ExpectStorage(m, t)
|
|
||||||
// ExpectErrorHandler(m, t)
|
|
||||||
// return m
|
|
||||||
// }
|
|
||||||
|
|
||||||
func ExpectDecoder(a op.Authorizer) {
|
func ExpectDecoder(a op.Authorizer) {
|
||||||
mockA := a.(*MockAuthorizer)
|
mockA := a.(*MockAuthorizer)
|
||||||
mockA.EXPECT().Decoder().AnyTimes().Return(schema.NewDecoder())
|
mockA.EXPECT().Decoder().AnyTimes().Return(schema.NewDecoder())
|
||||||
|
@ -47,17 +37,18 @@ func ExpectEncoder(a op.Authorizer) {
|
||||||
mockA.EXPECT().Encoder().AnyTimes().Return(schema.NewEncoder())
|
mockA.EXPECT().Encoder().AnyTimes().Return(schema.NewEncoder())
|
||||||
}
|
}
|
||||||
|
|
||||||
func ExpectSigner(a op.Authorizer, t *testing.T) {
|
//
|
||||||
mockA := a.(*MockAuthorizer)
|
//func ExpectSigner(a op.Authorizer, t *testing.T) {
|
||||||
mockA.EXPECT().Signer().DoAndReturn(
|
// mockA := a.(*MockAuthorizer)
|
||||||
func() op.Signer {
|
// mockA.EXPECT().Signer().DoAndReturn(
|
||||||
return &Sig{}
|
// func() op.Signer {
|
||||||
})
|
// return &Sig{}
|
||||||
}
|
// })
|
||||||
|
//}
|
||||||
|
|
||||||
func ExpectVerifier(a op.Authorizer, t *testing.T) {
|
func ExpectVerifier(a op.Authorizer, t *testing.T) {
|
||||||
mockA := a.(*MockAuthorizer)
|
mockA := a.(*MockAuthorizer)
|
||||||
mockA.EXPECT().IDTokenHintVerifier().DoAndReturn(
|
mockA.EXPECT().IDTokenHintVerifier(gomock.Any()).DoAndReturn(
|
||||||
func() op.IDTokenHintVerifier {
|
func() op.IDTokenHintVerifier {
|
||||||
return op.NewIDTokenHintVerifier("", nil)
|
return op.NewIDTokenHintVerifier("", nil)
|
||||||
})
|
})
|
||||||
|
|
|
@ -5,8 +5,8 @@ import (
|
||||||
|
|
||||||
"github.com/golang/mock/gomock"
|
"github.com/golang/mock/gomock"
|
||||||
|
|
||||||
"github.com/zitadel/oidc/pkg/oidc"
|
"github.com/zitadel/oidc/v2/pkg/oidc"
|
||||||
"github.com/zitadel/oidc/pkg/op"
|
"github.com/zitadel/oidc/v2/pkg/op"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewClient(t *testing.T) op.Client {
|
func NewClient(t *testing.T) op.Client {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
// Code generated by MockGen. DO NOT EDIT.
|
// Code generated by MockGen. DO NOT EDIT.
|
||||||
// Source: github.com/zitadel/oidc/pkg/op (interfaces: Client)
|
// Source: github.com/zitadel/oidc/v2/pkg/op (interfaces: Client)
|
||||||
|
|
||||||
// Package mock is a generated GoMock package.
|
// Package mock is a generated GoMock package.
|
||||||
package mock
|
package mock
|
||||||
|
@ -9,8 +9,8 @@ import (
|
||||||
time "time"
|
time "time"
|
||||||
|
|
||||||
gomock "github.com/golang/mock/gomock"
|
gomock "github.com/golang/mock/gomock"
|
||||||
oidc "github.com/zitadel/oidc/pkg/oidc"
|
oidc "github.com/zitadel/oidc/v2/pkg/oidc"
|
||||||
op "github.com/zitadel/oidc/pkg/op"
|
op "github.com/zitadel/oidc/v2/pkg/op"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MockClient is a mock of Client interface.
|
// MockClient is a mock of Client interface.
|
||||||
|
|
|
@ -1,14 +1,15 @@
|
||||||
// Code generated by MockGen. DO NOT EDIT.
|
// Code generated by MockGen. DO NOT EDIT.
|
||||||
// Source: github.com/zitadel/oidc/pkg/op (interfaces: Configuration)
|
// Source: github.com/zitadel/oidc/v2/pkg/op (interfaces: Configuration)
|
||||||
|
|
||||||
// Package mock is a generated GoMock package.
|
// Package mock is a generated GoMock package.
|
||||||
package mock
|
package mock
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
http "net/http"
|
||||||
reflect "reflect"
|
reflect "reflect"
|
||||||
|
|
||||||
gomock "github.com/golang/mock/gomock"
|
gomock "github.com/golang/mock/gomock"
|
||||||
op "github.com/zitadel/oidc/pkg/op"
|
op "github.com/zitadel/oidc/v2/pkg/op"
|
||||||
language "golang.org/x/text/language"
|
language "golang.org/x/text/language"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -91,6 +92,34 @@ func (mr *MockConfigurationMockRecorder) CodeMethodS256Supported() *gomock.Call
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CodeMethodS256Supported", reflect.TypeOf((*MockConfiguration)(nil).CodeMethodS256Supported))
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CodeMethodS256Supported", reflect.TypeOf((*MockConfiguration)(nil).CodeMethodS256Supported))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeviceAuthorization mocks base method.
|
||||||
|
func (m *MockConfiguration) DeviceAuthorization() op.DeviceAuthorizationConfig {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "DeviceAuthorization")
|
||||||
|
ret0, _ := ret[0].(op.DeviceAuthorizationConfig)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeviceAuthorization indicates an expected call of DeviceAuthorization.
|
||||||
|
func (mr *MockConfigurationMockRecorder) DeviceAuthorization() *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeviceAuthorization", reflect.TypeOf((*MockConfiguration)(nil).DeviceAuthorization))
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeviceAuthorizationEndpoint mocks base method.
|
||||||
|
func (m *MockConfiguration) DeviceAuthorizationEndpoint() op.Endpoint {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "DeviceAuthorizationEndpoint")
|
||||||
|
ret0, _ := ret[0].(op.Endpoint)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeviceAuthorizationEndpoint indicates an expected call of DeviceAuthorizationEndpoint.
|
||||||
|
func (mr *MockConfigurationMockRecorder) DeviceAuthorizationEndpoint() *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeviceAuthorizationEndpoint", reflect.TypeOf((*MockConfiguration)(nil).DeviceAuthorizationEndpoint))
|
||||||
|
}
|
||||||
|
|
||||||
// EndSessionEndpoint mocks base method.
|
// EndSessionEndpoint mocks base method.
|
||||||
func (m *MockConfiguration) EndSessionEndpoint() op.Endpoint {
|
func (m *MockConfiguration) EndSessionEndpoint() op.Endpoint {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
|
@ -119,6 +148,20 @@ func (mr *MockConfigurationMockRecorder) GrantTypeClientCredentialsSupported() *
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GrantTypeClientCredentialsSupported", reflect.TypeOf((*MockConfiguration)(nil).GrantTypeClientCredentialsSupported))
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GrantTypeClientCredentialsSupported", reflect.TypeOf((*MockConfiguration)(nil).GrantTypeClientCredentialsSupported))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GrantTypeDeviceCodeSupported mocks base method.
|
||||||
|
func (m *MockConfiguration) GrantTypeDeviceCodeSupported() bool {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "GrantTypeDeviceCodeSupported")
|
||||||
|
ret0, _ := ret[0].(bool)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// GrantTypeDeviceCodeSupported indicates an expected call of GrantTypeDeviceCodeSupported.
|
||||||
|
func (mr *MockConfigurationMockRecorder) GrantTypeDeviceCodeSupported() *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GrantTypeDeviceCodeSupported", reflect.TypeOf((*MockConfiguration)(nil).GrantTypeDeviceCodeSupported))
|
||||||
|
}
|
||||||
|
|
||||||
// GrantTypeJWTAuthorizationSupported mocks base method.
|
// GrantTypeJWTAuthorizationSupported mocks base method.
|
||||||
func (m *MockConfiguration) GrantTypeJWTAuthorizationSupported() bool {
|
func (m *MockConfiguration) GrantTypeJWTAuthorizationSupported() bool {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
|
@ -161,6 +204,20 @@ func (mr *MockConfigurationMockRecorder) GrantTypeTokenExchangeSupported() *gomo
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GrantTypeTokenExchangeSupported", reflect.TypeOf((*MockConfiguration)(nil).GrantTypeTokenExchangeSupported))
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GrantTypeTokenExchangeSupported", reflect.TypeOf((*MockConfiguration)(nil).GrantTypeTokenExchangeSupported))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Insecure mocks base method.
|
||||||
|
func (m *MockConfiguration) Insecure() bool {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "Insecure")
|
||||||
|
ret0, _ := ret[0].(bool)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insecure indicates an expected call of Insecure.
|
||||||
|
func (mr *MockConfigurationMockRecorder) Insecure() *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Insecure", reflect.TypeOf((*MockConfiguration)(nil).Insecure))
|
||||||
|
}
|
||||||
|
|
||||||
// IntrospectionAuthMethodPrivateKeyJWTSupported mocks base method.
|
// IntrospectionAuthMethodPrivateKeyJWTSupported mocks base method.
|
||||||
func (m *MockConfiguration) IntrospectionAuthMethodPrivateKeyJWTSupported() bool {
|
func (m *MockConfiguration) IntrospectionAuthMethodPrivateKeyJWTSupported() bool {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
|
@ -203,18 +260,18 @@ func (mr *MockConfigurationMockRecorder) IntrospectionEndpointSigningAlgorithmsS
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IntrospectionEndpointSigningAlgorithmsSupported", reflect.TypeOf((*MockConfiguration)(nil).IntrospectionEndpointSigningAlgorithmsSupported))
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IntrospectionEndpointSigningAlgorithmsSupported", reflect.TypeOf((*MockConfiguration)(nil).IntrospectionEndpointSigningAlgorithmsSupported))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Issuer mocks base method.
|
// IssuerFromRequest mocks base method.
|
||||||
func (m *MockConfiguration) Issuer() string {
|
func (m *MockConfiguration) IssuerFromRequest(arg0 *http.Request) string {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
ret := m.ctrl.Call(m, "Issuer")
|
ret := m.ctrl.Call(m, "IssuerFromRequest", arg0)
|
||||||
ret0, _ := ret[0].(string)
|
ret0, _ := ret[0].(string)
|
||||||
return ret0
|
return ret0
|
||||||
}
|
}
|
||||||
|
|
||||||
// Issuer indicates an expected call of Issuer.
|
// IssuerFromRequest indicates an expected call of IssuerFromRequest.
|
||||||
func (mr *MockConfigurationMockRecorder) Issuer() *gomock.Call {
|
func (mr *MockConfigurationMockRecorder) IssuerFromRequest(arg0 interface{}) *gomock.Call {
|
||||||
mr.mock.ctrl.T.Helper()
|
mr.mock.ctrl.T.Helper()
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Issuer", reflect.TypeOf((*MockConfiguration)(nil).Issuer))
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IssuerFromRequest", reflect.TypeOf((*MockConfiguration)(nil).IssuerFromRequest), arg0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// KeysEndpoint mocks base method.
|
// KeysEndpoint mocks base method.
|
||||||
|
|
51
pkg/op/mock/discovery.mock.go
Normal file
51
pkg/op/mock/discovery.mock.go
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
// Code generated by MockGen. DO NOT EDIT.
|
||||||
|
// Source: github.com/zitadel/oidc/v2/pkg/op (interfaces: DiscoverStorage)
|
||||||
|
|
||||||
|
// Package mock is a generated GoMock package.
|
||||||
|
package mock
|
||||||
|
|
||||||
|
import (
|
||||||
|
context "context"
|
||||||
|
reflect "reflect"
|
||||||
|
|
||||||
|
gomock "github.com/golang/mock/gomock"
|
||||||
|
jose "gopkg.in/square/go-jose.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MockDiscoverStorage is a mock of DiscoverStorage interface.
|
||||||
|
type MockDiscoverStorage struct {
|
||||||
|
ctrl *gomock.Controller
|
||||||
|
recorder *MockDiscoverStorageMockRecorder
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockDiscoverStorageMockRecorder is the mock recorder for MockDiscoverStorage.
|
||||||
|
type MockDiscoverStorageMockRecorder struct {
|
||||||
|
mock *MockDiscoverStorage
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMockDiscoverStorage creates a new mock instance.
|
||||||
|
func NewMockDiscoverStorage(ctrl *gomock.Controller) *MockDiscoverStorage {
|
||||||
|
mock := &MockDiscoverStorage{ctrl: ctrl}
|
||||||
|
mock.recorder = &MockDiscoverStorageMockRecorder{mock}
|
||||||
|
return mock
|
||||||
|
}
|
||||||
|
|
||||||
|
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||||
|
func (m *MockDiscoverStorage) EXPECT() *MockDiscoverStorageMockRecorder {
|
||||||
|
return m.recorder
|
||||||
|
}
|
||||||
|
|
||||||
|
// SignatureAlgorithms mocks base method.
|
||||||
|
func (m *MockDiscoverStorage) SignatureAlgorithms(arg0 context.Context) ([]jose.SignatureAlgorithm, error) {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "SignatureAlgorithms", arg0)
|
||||||
|
ret0, _ := ret[0].([]jose.SignatureAlgorithm)
|
||||||
|
ret1, _ := ret[1].(error)
|
||||||
|
return ret0, ret1
|
||||||
|
}
|
||||||
|
|
||||||
|
// SignatureAlgorithms indicates an expected call of SignatureAlgorithms.
|
||||||
|
func (mr *MockDiscoverStorageMockRecorder) SignatureAlgorithms(arg0 interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SignatureAlgorithms", reflect.TypeOf((*MockDiscoverStorage)(nil).SignatureAlgorithms), arg0)
|
||||||
|
}
|
|
@ -1,8 +1,10 @@
|
||||||
package mock
|
package mock
|
||||||
|
|
||||||
//go:generate mockgen -package mock -destination ./storage.mock.go github.com/zitadel/oidc/pkg/op Storage
|
//go:generate go install github.com/golang/mock/mockgen@v1.6.0
|
||||||
//go:generate mockgen -package mock -destination ./authorizer.mock.go github.com/zitadel/oidc/pkg/op Authorizer
|
//go:generate mockgen -package mock -destination ./storage.mock.go github.com/zitadel/oidc/v2/pkg/op Storage
|
||||||
//go:generate mockgen -package mock -destination ./client.mock.go github.com/zitadel/oidc/pkg/op Client
|
//go:generate mockgen -package mock -destination ./authorizer.mock.go github.com/zitadel/oidc/v2/pkg/op Authorizer
|
||||||
//go:generate mockgen -package mock -destination ./configuration.mock.go github.com/zitadel/oidc/pkg/op Configuration
|
//go:generate mockgen -package mock -destination ./client.mock.go github.com/zitadel/oidc/v2/pkg/op Client
|
||||||
//go:generate mockgen -package mock -destination ./signer.mock.go github.com/zitadel/oidc/pkg/op Signer
|
//go:generate mockgen -package mock -destination ./configuration.mock.go github.com/zitadel/oidc/v2/pkg/op Configuration
|
||||||
//go:generate mockgen -package mock -destination ./key.mock.go github.com/zitadel/oidc/pkg/op KeyProvider
|
//go:generate mockgen -package mock -destination ./discovery.mock.go github.com/zitadel/oidc/v2/pkg/op DiscoverStorage
|
||||||
|
//go:generate mockgen -package mock -destination ./signer.mock.go github.com/zitadel/oidc/v2/pkg/op SigningKey,Key
|
||||||
|
//go:generate mockgen -package mock -destination ./key.mock.go github.com/zitadel/oidc/v2/pkg/op KeyProvider
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
// Code generated by MockGen. DO NOT EDIT.
|
// Code generated by MockGen. DO NOT EDIT.
|
||||||
// Source: github.com/zitadel/oidc/pkg/op (interfaces: KeyProvider)
|
// Source: github.com/zitadel/oidc/v2/pkg/op (interfaces: KeyProvider)
|
||||||
|
|
||||||
// Package mock is a generated GoMock package.
|
// Package mock is a generated GoMock package.
|
||||||
package mock
|
package mock
|
||||||
|
@ -9,7 +9,7 @@ import (
|
||||||
reflect "reflect"
|
reflect "reflect"
|
||||||
|
|
||||||
gomock "github.com/golang/mock/gomock"
|
gomock "github.com/golang/mock/gomock"
|
||||||
jose "gopkg.in/square/go-jose.v2"
|
op "github.com/zitadel/oidc/v2/pkg/op"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MockKeyProvider is a mock of KeyProvider interface.
|
// MockKeyProvider is a mock of KeyProvider interface.
|
||||||
|
@ -35,17 +35,17 @@ func (m *MockKeyProvider) EXPECT() *MockKeyProviderMockRecorder {
|
||||||
return m.recorder
|
return m.recorder
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetKeySet mocks base method.
|
// KeySet mocks base method.
|
||||||
func (m *MockKeyProvider) GetKeySet(arg0 context.Context) (*jose.JSONWebKeySet, error) {
|
func (m *MockKeyProvider) KeySet(arg0 context.Context) ([]op.Key, error) {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
ret := m.ctrl.Call(m, "GetKeySet", arg0)
|
ret := m.ctrl.Call(m, "KeySet", arg0)
|
||||||
ret0, _ := ret[0].(*jose.JSONWebKeySet)
|
ret0, _ := ret[0].([]op.Key)
|
||||||
ret1, _ := ret[1].(error)
|
ret1, _ := ret[1].(error)
|
||||||
return ret0, ret1
|
return ret0, ret1
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetKeySet indicates an expected call of GetKeySet.
|
// KeySet indicates an expected call of KeySet.
|
||||||
func (mr *MockKeyProviderMockRecorder) GetKeySet(arg0 interface{}) *gomock.Call {
|
func (mr *MockKeyProviderMockRecorder) KeySet(arg0 interface{}) *gomock.Call {
|
||||||
mr.mock.ctrl.T.Helper()
|
mr.mock.ctrl.T.Helper()
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetKeySet", reflect.TypeOf((*MockKeyProvider)(nil).GetKeySet), arg0)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "KeySet", reflect.TypeOf((*MockKeyProvider)(nil).KeySet), arg0)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,56 +1,69 @@
|
||||||
// Code generated by MockGen. DO NOT EDIT.
|
// Code generated by MockGen. DO NOT EDIT.
|
||||||
// Source: github.com/zitadel/oidc/pkg/op (interfaces: Signer)
|
// Source: github.com/zitadel/oidc/v2/pkg/op (interfaces: SigningKey,Key)
|
||||||
|
|
||||||
// Package mock is a generated GoMock package.
|
// Package mock is a generated GoMock package.
|
||||||
package mock
|
package mock
|
||||||
|
|
||||||
import (
|
import (
|
||||||
context "context"
|
|
||||||
reflect "reflect"
|
reflect "reflect"
|
||||||
|
|
||||||
gomock "github.com/golang/mock/gomock"
|
gomock "github.com/golang/mock/gomock"
|
||||||
jose "gopkg.in/square/go-jose.v2"
|
jose "gopkg.in/square/go-jose.v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MockSigner is a mock of Signer interface.
|
// MockSigningKey is a mock of SigningKey interface.
|
||||||
type MockSigner struct {
|
type MockSigningKey struct {
|
||||||
ctrl *gomock.Controller
|
ctrl *gomock.Controller
|
||||||
recorder *MockSignerMockRecorder
|
recorder *MockSigningKeyMockRecorder
|
||||||
}
|
}
|
||||||
|
|
||||||
// MockSignerMockRecorder is the mock recorder for MockSigner.
|
// MockSigningKeyMockRecorder is the mock recorder for MockSigningKey.
|
||||||
type MockSignerMockRecorder struct {
|
type MockSigningKeyMockRecorder struct {
|
||||||
mock *MockSigner
|
mock *MockSigningKey
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewMockSigner creates a new mock instance.
|
// NewMockSigningKey creates a new mock instance.
|
||||||
func NewMockSigner(ctrl *gomock.Controller) *MockSigner {
|
func NewMockSigningKey(ctrl *gomock.Controller) *MockSigningKey {
|
||||||
mock := &MockSigner{ctrl: ctrl}
|
mock := &MockSigningKey{ctrl: ctrl}
|
||||||
mock.recorder = &MockSignerMockRecorder{mock}
|
mock.recorder = &MockSigningKeyMockRecorder{mock}
|
||||||
return mock
|
return mock
|
||||||
}
|
}
|
||||||
|
|
||||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||||
func (m *MockSigner) EXPECT() *MockSignerMockRecorder {
|
func (m *MockSigningKey) EXPECT() *MockSigningKeyMockRecorder {
|
||||||
return m.recorder
|
return m.recorder
|
||||||
}
|
}
|
||||||
|
|
||||||
// Health mocks base method.
|
// ID mocks base method.
|
||||||
func (m *MockSigner) Health(arg0 context.Context) error {
|
func (m *MockSigningKey) ID() string {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
ret := m.ctrl.Call(m, "Health", arg0)
|
ret := m.ctrl.Call(m, "ID")
|
||||||
ret0, _ := ret[0].(error)
|
ret0, _ := ret[0].(string)
|
||||||
return ret0
|
return ret0
|
||||||
}
|
}
|
||||||
|
|
||||||
// Health indicates an expected call of Health.
|
// ID indicates an expected call of ID.
|
||||||
func (mr *MockSignerMockRecorder) Health(arg0 interface{}) *gomock.Call {
|
func (mr *MockSigningKeyMockRecorder) ID() *gomock.Call {
|
||||||
mr.mock.ctrl.T.Helper()
|
mr.mock.ctrl.T.Helper()
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Health", reflect.TypeOf((*MockSigner)(nil).Health), arg0)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ID", reflect.TypeOf((*MockSigningKey)(nil).ID))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Key mocks base method.
|
||||||
|
func (m *MockSigningKey) Key() interface{} {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "Key")
|
||||||
|
ret0, _ := ret[0].(interface{})
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Key indicates an expected call of Key.
|
||||||
|
func (mr *MockSigningKeyMockRecorder) Key() *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Key", reflect.TypeOf((*MockSigningKey)(nil).Key))
|
||||||
}
|
}
|
||||||
|
|
||||||
// SignatureAlgorithm mocks base method.
|
// SignatureAlgorithm mocks base method.
|
||||||
func (m *MockSigner) SignatureAlgorithm() jose.SignatureAlgorithm {
|
func (m *MockSigningKey) SignatureAlgorithm() jose.SignatureAlgorithm {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
ret := m.ctrl.Call(m, "SignatureAlgorithm")
|
ret := m.ctrl.Call(m, "SignatureAlgorithm")
|
||||||
ret0, _ := ret[0].(jose.SignatureAlgorithm)
|
ret0, _ := ret[0].(jose.SignatureAlgorithm)
|
||||||
|
@ -58,21 +71,86 @@ func (m *MockSigner) SignatureAlgorithm() jose.SignatureAlgorithm {
|
||||||
}
|
}
|
||||||
|
|
||||||
// SignatureAlgorithm indicates an expected call of SignatureAlgorithm.
|
// SignatureAlgorithm indicates an expected call of SignatureAlgorithm.
|
||||||
func (mr *MockSignerMockRecorder) SignatureAlgorithm() *gomock.Call {
|
func (mr *MockSigningKeyMockRecorder) SignatureAlgorithm() *gomock.Call {
|
||||||
mr.mock.ctrl.T.Helper()
|
mr.mock.ctrl.T.Helper()
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SignatureAlgorithm", reflect.TypeOf((*MockSigner)(nil).SignatureAlgorithm))
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SignatureAlgorithm", reflect.TypeOf((*MockSigningKey)(nil).SignatureAlgorithm))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Signer mocks base method.
|
// MockKey is a mock of Key interface.
|
||||||
func (m *MockSigner) Signer() jose.Signer {
|
type MockKey struct {
|
||||||
|
ctrl *gomock.Controller
|
||||||
|
recorder *MockKeyMockRecorder
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockKeyMockRecorder is the mock recorder for MockKey.
|
||||||
|
type MockKeyMockRecorder struct {
|
||||||
|
mock *MockKey
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMockKey creates a new mock instance.
|
||||||
|
func NewMockKey(ctrl *gomock.Controller) *MockKey {
|
||||||
|
mock := &MockKey{ctrl: ctrl}
|
||||||
|
mock.recorder = &MockKeyMockRecorder{mock}
|
||||||
|
return mock
|
||||||
|
}
|
||||||
|
|
||||||
|
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||||
|
func (m *MockKey) EXPECT() *MockKeyMockRecorder {
|
||||||
|
return m.recorder
|
||||||
|
}
|
||||||
|
|
||||||
|
// Algorithm mocks base method.
|
||||||
|
func (m *MockKey) Algorithm() jose.SignatureAlgorithm {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
ret := m.ctrl.Call(m, "Signer")
|
ret := m.ctrl.Call(m, "Algorithm")
|
||||||
ret0, _ := ret[0].(jose.Signer)
|
ret0, _ := ret[0].(jose.SignatureAlgorithm)
|
||||||
return ret0
|
return ret0
|
||||||
}
|
}
|
||||||
|
|
||||||
// Signer indicates an expected call of Signer.
|
// Algorithm indicates an expected call of Algorithm.
|
||||||
func (mr *MockSignerMockRecorder) Signer() *gomock.Call {
|
func (mr *MockKeyMockRecorder) Algorithm() *gomock.Call {
|
||||||
mr.mock.ctrl.T.Helper()
|
mr.mock.ctrl.T.Helper()
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Signer", reflect.TypeOf((*MockSigner)(nil).Signer))
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Algorithm", reflect.TypeOf((*MockKey)(nil).Algorithm))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ID mocks base method.
|
||||||
|
func (m *MockKey) ID() string {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "ID")
|
||||||
|
ret0, _ := ret[0].(string)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// ID indicates an expected call of ID.
|
||||||
|
func (mr *MockKeyMockRecorder) ID() *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ID", reflect.TypeOf((*MockKey)(nil).ID))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Key mocks base method.
|
||||||
|
func (m *MockKey) Key() interface{} {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "Key")
|
||||||
|
ret0, _ := ret[0].(interface{})
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Key indicates an expected call of Key.
|
||||||
|
func (mr *MockKeyMockRecorder) Key() *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Key", reflect.TypeOf((*MockKey)(nil).Key))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use mocks base method.
|
||||||
|
func (m *MockKey) Use() string {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "Use")
|
||||||
|
ret0, _ := ret[0].(string)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use indicates an expected call of Use.
|
||||||
|
func (mr *MockKeyMockRecorder) Use() *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Use", reflect.TypeOf((*MockKey)(nil).Use))
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
// Code generated by MockGen. DO NOT EDIT.
|
// Code generated by MockGen. DO NOT EDIT.
|
||||||
// Source: github.com/zitadel/oidc/pkg/op (interfaces: Storage)
|
// Source: github.com/zitadel/oidc/v2/pkg/op (interfaces: Storage)
|
||||||
|
|
||||||
// Package mock is a generated GoMock package.
|
// Package mock is a generated GoMock package.
|
||||||
package mock
|
package mock
|
||||||
|
@ -10,8 +10,8 @@ import (
|
||||||
time "time"
|
time "time"
|
||||||
|
|
||||||
gomock "github.com/golang/mock/gomock"
|
gomock "github.com/golang/mock/gomock"
|
||||||
oidc "github.com/zitadel/oidc/pkg/oidc"
|
oidc "github.com/zitadel/oidc/v2/pkg/oidc"
|
||||||
op "github.com/zitadel/oidc/pkg/op"
|
op "github.com/zitadel/oidc/v2/pkg/op"
|
||||||
jose "gopkg.in/square/go-jose.v2"
|
jose "gopkg.in/square/go-jose.v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -159,34 +159,19 @@ func (mr *MockStorageMockRecorder) GetClientByClientID(arg0, arg1 interface{}) *
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetClientByClientID", reflect.TypeOf((*MockStorage)(nil).GetClientByClientID), arg0, arg1)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetClientByClientID", reflect.TypeOf((*MockStorage)(nil).GetClientByClientID), arg0, arg1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetKeyByIDAndUserID mocks base method.
|
// GetKeyByIDAndClientID mocks base method.
|
||||||
func (m *MockStorage) GetKeyByIDAndUserID(arg0 context.Context, arg1, arg2 string) (*jose.JSONWebKey, error) {
|
func (m *MockStorage) GetKeyByIDAndClientID(arg0 context.Context, arg1, arg2 string) (*jose.JSONWebKey, error) {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
ret := m.ctrl.Call(m, "GetKeyByIDAndUserID", arg0, arg1, arg2)
|
ret := m.ctrl.Call(m, "GetKeyByIDAndClientID", arg0, arg1, arg2)
|
||||||
ret0, _ := ret[0].(*jose.JSONWebKey)
|
ret0, _ := ret[0].(*jose.JSONWebKey)
|
||||||
ret1, _ := ret[1].(error)
|
ret1, _ := ret[1].(error)
|
||||||
return ret0, ret1
|
return ret0, ret1
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetKeyByIDAndUserID indicates an expected call of GetKeyByIDAndUserID.
|
// GetKeyByIDAndClientID indicates an expected call of GetKeyByIDAndClientID.
|
||||||
func (mr *MockStorageMockRecorder) GetKeyByIDAndUserID(arg0, arg1, arg2 interface{}) *gomock.Call {
|
func (mr *MockStorageMockRecorder) GetKeyByIDAndClientID(arg0, arg1, arg2 interface{}) *gomock.Call {
|
||||||
mr.mock.ctrl.T.Helper()
|
mr.mock.ctrl.T.Helper()
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetKeyByIDAndUserID", reflect.TypeOf((*MockStorage)(nil).GetKeyByIDAndUserID), arg0, arg1, arg2)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetKeyByIDAndClientID", reflect.TypeOf((*MockStorage)(nil).GetKeyByIDAndClientID), arg0, arg1, arg2)
|
||||||
}
|
|
||||||
|
|
||||||
// GetKeySet mocks base method.
|
|
||||||
func (m *MockStorage) GetKeySet(arg0 context.Context) (*jose.JSONWebKeySet, error) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "GetKeySet", arg0)
|
|
||||||
ret0, _ := ret[0].(*jose.JSONWebKeySet)
|
|
||||||
ret1, _ := ret[1].(error)
|
|
||||||
return ret0, ret1
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetKeySet indicates an expected call of GetKeySet.
|
|
||||||
func (mr *MockStorageMockRecorder) GetKeySet(arg0 interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetKeySet", reflect.TypeOf((*MockStorage)(nil).GetKeySet), arg0)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetPrivateClaimsFromScopes mocks base method.
|
// GetPrivateClaimsFromScopes mocks base method.
|
||||||
|
@ -204,16 +189,20 @@ func (mr *MockStorageMockRecorder) GetPrivateClaimsFromScopes(arg0, arg1, arg2,
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPrivateClaimsFromScopes", reflect.TypeOf((*MockStorage)(nil).GetPrivateClaimsFromScopes), arg0, arg1, arg2, arg3)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPrivateClaimsFromScopes", reflect.TypeOf((*MockStorage)(nil).GetPrivateClaimsFromScopes), arg0, arg1, arg2, arg3)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetSigningKey mocks base method.
|
// GetRefreshTokenInfo mocks base method.
|
||||||
func (m *MockStorage) GetSigningKey(arg0 context.Context, arg1 chan<- jose.SigningKey) {
|
func (m *MockStorage) GetRefreshTokenInfo(arg0 context.Context, arg1, arg2 string) (string, string, error) {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
m.ctrl.Call(m, "GetSigningKey", arg0, arg1)
|
ret := m.ctrl.Call(m, "GetRefreshTokenInfo", arg0, arg1, arg2)
|
||||||
|
ret0, _ := ret[0].(string)
|
||||||
|
ret1, _ := ret[1].(string)
|
||||||
|
ret2, _ := ret[2].(error)
|
||||||
|
return ret0, ret1, ret2
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetSigningKey indicates an expected call of GetSigningKey.
|
// GetRefreshTokenInfo indicates an expected call of GetRefreshTokenInfo.
|
||||||
func (mr *MockStorageMockRecorder) GetSigningKey(arg0, arg1 interface{}) *gomock.Call {
|
func (mr *MockStorageMockRecorder) GetRefreshTokenInfo(arg0, arg1, arg2 interface{}) *gomock.Call {
|
||||||
mr.mock.ctrl.T.Helper()
|
mr.mock.ctrl.T.Helper()
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSigningKey", reflect.TypeOf((*MockStorage)(nil).GetSigningKey), arg0, arg1)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRefreshTokenInfo", reflect.TypeOf((*MockStorage)(nil).GetRefreshTokenInfo), arg0, arg1, arg2)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Health mocks base method.
|
// Health mocks base method.
|
||||||
|
@ -230,6 +219,21 @@ func (mr *MockStorageMockRecorder) Health(arg0 interface{}) *gomock.Call {
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Health", reflect.TypeOf((*MockStorage)(nil).Health), arg0)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Health", reflect.TypeOf((*MockStorage)(nil).Health), arg0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// KeySet mocks base method.
|
||||||
|
func (m *MockStorage) KeySet(arg0 context.Context) ([]op.Key, error) {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "KeySet", arg0)
|
||||||
|
ret0, _ := ret[0].([]op.Key)
|
||||||
|
ret1, _ := ret[1].(error)
|
||||||
|
return ret0, ret1
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeySet indicates an expected call of KeySet.
|
||||||
|
func (mr *MockStorageMockRecorder) KeySet(arg0 interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "KeySet", reflect.TypeOf((*MockStorage)(nil).KeySet), arg0)
|
||||||
|
}
|
||||||
|
|
||||||
// RevokeToken mocks base method.
|
// RevokeToken mocks base method.
|
||||||
func (m *MockStorage) RevokeToken(arg0 context.Context, arg1, arg2, arg3 string) *oidc.Error {
|
func (m *MockStorage) RevokeToken(arg0 context.Context, arg1, arg2, arg3 string) *oidc.Error {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
|
@ -259,7 +263,7 @@ func (mr *MockStorageMockRecorder) SaveAuthCode(arg0, arg1, arg2 interface{}) *g
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetIntrospectionFromToken mocks base method.
|
// SetIntrospectionFromToken mocks base method.
|
||||||
func (m *MockStorage) SetIntrospectionFromToken(arg0 context.Context, arg1 oidc.IntrospectionResponse, arg2, arg3, arg4 string) error {
|
func (m *MockStorage) SetIntrospectionFromToken(arg0 context.Context, arg1 *oidc.IntrospectionResponse, arg2, arg3, arg4 string) error {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
ret := m.ctrl.Call(m, "SetIntrospectionFromToken", arg0, arg1, arg2, arg3, arg4)
|
ret := m.ctrl.Call(m, "SetIntrospectionFromToken", arg0, arg1, arg2, arg3, arg4)
|
||||||
ret0, _ := ret[0].(error)
|
ret0, _ := ret[0].(error)
|
||||||
|
@ -273,7 +277,7 @@ func (mr *MockStorageMockRecorder) SetIntrospectionFromToken(arg0, arg1, arg2, a
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetUserinfoFromScopes mocks base method.
|
// SetUserinfoFromScopes mocks base method.
|
||||||
func (m *MockStorage) SetUserinfoFromScopes(arg0 context.Context, arg1 oidc.UserInfoSetter, arg2, arg3 string, arg4 []string) error {
|
func (m *MockStorage) SetUserinfoFromScopes(arg0 context.Context, arg1 *oidc.UserInfo, arg2, arg3 string, arg4 []string) error {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
ret := m.ctrl.Call(m, "SetUserinfoFromScopes", arg0, arg1, arg2, arg3, arg4)
|
ret := m.ctrl.Call(m, "SetUserinfoFromScopes", arg0, arg1, arg2, arg3, arg4)
|
||||||
ret0, _ := ret[0].(error)
|
ret0, _ := ret[0].(error)
|
||||||
|
@ -287,7 +291,7 @@ func (mr *MockStorageMockRecorder) SetUserinfoFromScopes(arg0, arg1, arg2, arg3,
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetUserinfoFromToken mocks base method.
|
// SetUserinfoFromToken mocks base method.
|
||||||
func (m *MockStorage) SetUserinfoFromToken(arg0 context.Context, arg1 oidc.UserInfoSetter, arg2, arg3, arg4 string) error {
|
func (m *MockStorage) SetUserinfoFromToken(arg0 context.Context, arg1 *oidc.UserInfo, arg2, arg3, arg4 string) error {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
ret := m.ctrl.Call(m, "SetUserinfoFromToken", arg0, arg1, arg2, arg3, arg4)
|
ret := m.ctrl.Call(m, "SetUserinfoFromToken", arg0, arg1, arg2, arg3, arg4)
|
||||||
ret0, _ := ret[0].(error)
|
ret0, _ := ret[0].(error)
|
||||||
|
@ -300,6 +304,36 @@ func (mr *MockStorageMockRecorder) SetUserinfoFromToken(arg0, arg1, arg2, arg3,
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetUserinfoFromToken", reflect.TypeOf((*MockStorage)(nil).SetUserinfoFromToken), arg0, arg1, arg2, arg3, arg4)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetUserinfoFromToken", reflect.TypeOf((*MockStorage)(nil).SetUserinfoFromToken), arg0, arg1, arg2, arg3, arg4)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SignatureAlgorithms mocks base method.
|
||||||
|
func (m *MockStorage) SignatureAlgorithms(arg0 context.Context) ([]jose.SignatureAlgorithm, error) {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "SignatureAlgorithms", arg0)
|
||||||
|
ret0, _ := ret[0].([]jose.SignatureAlgorithm)
|
||||||
|
ret1, _ := ret[1].(error)
|
||||||
|
return ret0, ret1
|
||||||
|
}
|
||||||
|
|
||||||
|
// SignatureAlgorithms indicates an expected call of SignatureAlgorithms.
|
||||||
|
func (mr *MockStorageMockRecorder) SignatureAlgorithms(arg0 interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SignatureAlgorithms", reflect.TypeOf((*MockStorage)(nil).SignatureAlgorithms), arg0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SigningKey mocks base method.
|
||||||
|
func (m *MockStorage) SigningKey(arg0 context.Context) (op.SigningKey, error) {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "SigningKey", arg0)
|
||||||
|
ret0, _ := ret[0].(op.SigningKey)
|
||||||
|
ret1, _ := ret[1].(error)
|
||||||
|
return ret0, ret1
|
||||||
|
}
|
||||||
|
|
||||||
|
// SigningKey indicates an expected call of SigningKey.
|
||||||
|
func (mr *MockStorageMockRecorder) SigningKey(arg0 interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SigningKey", reflect.TypeOf((*MockStorage)(nil).SigningKey), arg0)
|
||||||
|
}
|
||||||
|
|
||||||
// TerminateSession mocks base method.
|
// TerminateSession mocks base method.
|
||||||
func (m *MockStorage) TerminateSession(arg0 context.Context, arg1, arg2 string) error {
|
func (m *MockStorage) TerminateSession(arg0 context.Context, arg1, arg2 string) error {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
|
|
|
@ -6,13 +6,10 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/zitadel/oidc/pkg/oidc"
|
|
||||||
|
|
||||||
"gopkg.in/square/go-jose.v2"
|
|
||||||
|
|
||||||
"github.com/golang/mock/gomock"
|
"github.com/golang/mock/gomock"
|
||||||
|
|
||||||
"github.com/zitadel/oidc/pkg/op"
|
"github.com/zitadel/oidc/v2/pkg/oidc"
|
||||||
|
"github.com/zitadel/oidc/v2/pkg/op"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewStorage(t *testing.T) op.Storage {
|
func NewStorage(t *testing.T) op.Storage {
|
||||||
|
@ -41,13 +38,13 @@ func NewMockStorageAny(t *testing.T) op.Storage {
|
||||||
|
|
||||||
func NewMockStorageSigningKeyInvalid(t *testing.T) op.Storage {
|
func NewMockStorageSigningKeyInvalid(t *testing.T) op.Storage {
|
||||||
m := NewStorage(t)
|
m := NewStorage(t)
|
||||||
ExpectSigningKeyInvalid(m)
|
//ExpectSigningKeyInvalid(m)
|
||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMockStorageSigningKey(t *testing.T) op.Storage {
|
func NewMockStorageSigningKey(t *testing.T) op.Storage {
|
||||||
m := NewStorage(t)
|
m := NewStorage(t)
|
||||||
ExpectSigningKey(m)
|
//ExpectSigningKey(m)
|
||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -85,24 +82,6 @@ func ExpectValidClientID(s op.Storage) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func ExpectSigningKeyInvalid(s op.Storage) {
|
|
||||||
mockS := s.(*MockStorage)
|
|
||||||
mockS.EXPECT().GetSigningKey(gomock.Any(), gomock.Any()).DoAndReturn(
|
|
||||||
func(_ context.Context, keyCh chan<- jose.SigningKey) {
|
|
||||||
keyCh <- jose.SigningKey{}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func ExpectSigningKey(s op.Storage) {
|
|
||||||
mockS := s.(*MockStorage)
|
|
||||||
mockS.EXPECT().GetSigningKey(gomock.Any(), gomock.Any()).DoAndReturn(
|
|
||||||
func(_ context.Context, keyCh chan<- jose.SigningKey) {
|
|
||||||
keyCh <- jose.SigningKey{Algorithm: jose.HS256, Key: []byte("key")}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
type ConfClient struct {
|
type ConfClient struct {
|
||||||
id string
|
id string
|
||||||
appType op.ApplicationType
|
appType op.ApplicationType
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue