feat: token introspection (#83)
* introspect * introspect and client assertion * introspect and client assertion * scopes * token introspection * introspect * refactoring * fixes * clenaup * Update example/internal/mock/storage.go Co-authored-by: Fabi <38692350+fgerschwiler@users.noreply.github.com> * clenaup Co-authored-by: Fabi <38692350+fgerschwiler@users.noreply.github.com>
This commit is contained in:
parent
fa92a20615
commit
1518c843de
46 changed files with 1672 additions and 570 deletions
|
@ -1,90 +1,103 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
// import (
|
import (
|
||||||
// "encoding/json"
|
"encoding/json"
|
||||||
// "fmt"
|
"fmt"
|
||||||
// "log"
|
"log"
|
||||||
// "net/http"
|
"net/http"
|
||||||
// "os"
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
// "github.com/caos/oidc/pkg/oidc"
|
"github.com/gorilla/mux"
|
||||||
// "github.com/caos/oidc/pkg/oidc/rp"
|
"github.com/sirupsen/logrus"
|
||||||
// "github.com/caos/utils/logging"
|
|
||||||
// )
|
|
||||||
|
|
||||||
// const (
|
"github.com/caos/oidc/pkg/client/rs"
|
||||||
// publicURL string = "/public"
|
"github.com/caos/oidc/pkg/oidc"
|
||||||
// protectedURL string = "/protected"
|
)
|
||||||
// protectedExchangeURL string = "/protected/exchange"
|
|
||||||
// )
|
const (
|
||||||
|
publicURL string = "/public"
|
||||||
|
protectedURL string = "/protected"
|
||||||
|
protectedClaimURL string = "/protected/{claim}/{value}"
|
||||||
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// clientID := os.Getenv("CLIENT_ID")
|
keyPath := os.Getenv("KEY")
|
||||||
// clientSecret := os.Getenv("CLIENT_SECRET")
|
port := os.Getenv("PORT")
|
||||||
// issuer := os.Getenv("ISSUER")
|
issuer := os.Getenv("ISSUER")
|
||||||
// port := os.Getenv("PORT")
|
|
||||||
|
|
||||||
// // ctx := context.Background()
|
provider, err := rs.NewResourceServerFromKeyFile(issuer, keyPath)
|
||||||
|
if err != nil {
|
||||||
|
logrus.Fatalf("error creating provider %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
// providerConfig := &oidc.ProviderConfig{
|
router := mux.NewRouter()
|
||||||
// ClientID: clientID,
|
|
||||||
// ClientSecret: clientSecret,
|
|
||||||
// Issuer: issuer,
|
|
||||||
// }
|
|
||||||
// provider, err := rp.NewDefaultProvider(providerConfig)
|
|
||||||
// logging.Log("APP-nx6PeF").OnError(err).Panic("error creating provider")
|
|
||||||
|
|
||||||
// http.HandleFunc(publicURL, func(w http.ResponseWriter, r *http.Request) {
|
//public url accessible without any authorization
|
||||||
// w.Write([]byte("OK"))
|
//will print `OK` and current timestamp
|
||||||
// })
|
router.HandleFunc(publicURL, func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Write([]byte("OK " + time.Now().String()))
|
||||||
|
})
|
||||||
|
|
||||||
// http.HandleFunc(protectedURL, func(w http.ResponseWriter, r *http.Request) {
|
//protected url which needs an active token
|
||||||
// ok, token := checkToken(w, r)
|
//will print the result of the introspection endpoint on success
|
||||||
// if !ok {
|
router.HandleFunc(protectedURL, func(w http.ResponseWriter, r *http.Request) {
|
||||||
// return
|
ok, token := checkToken(w, r)
|
||||||
// }
|
if !ok {
|
||||||
// resp, err := provider.Introspect(r.Context(), token)
|
return
|
||||||
// if err != nil {
|
}
|
||||||
// http.Error(w, err.Error(), http.StatusForbidden)
|
resp, err := rs.Introspect(r.Context(), provider, token)
|
||||||
// return
|
if err != nil {
|
||||||
// }
|
http.Error(w, err.Error(), http.StatusForbidden)
|
||||||
// data, err := json.Marshal(resp)
|
return
|
||||||
// if err != nil {
|
}
|
||||||
// http.Error(w, err.Error(), http.StatusInternalServerError)
|
data, err := json.Marshal(resp)
|
||||||
// return
|
if err != nil {
|
||||||
// }
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
// w.Write(data)
|
return
|
||||||
// })
|
}
|
||||||
|
w.Write(data)
|
||||||
|
})
|
||||||
|
|
||||||
// http.HandleFunc(protectedExchangeURL, func(w http.ResponseWriter, r *http.Request) {
|
//protected url which needs an active token and checks if the response of the introspect endpoint
|
||||||
// ok, token := checkToken(w, r)
|
//contains a requested claim with the required (string) value
|
||||||
// if !ok {
|
//e.g. /protected/username/livio@caos.ch
|
||||||
// return
|
router.HandleFunc(protectedClaimURL, func(w http.ResponseWriter, r *http.Request) {
|
||||||
// }
|
ok, token := checkToken(w, r)
|
||||||
// tokens, err := provider.DelegationTokenExchange(r.Context(), token, oidc.WithResource([]string{"Test"}))
|
if !ok {
|
||||||
// if err != nil {
|
return
|
||||||
// http.Error(w, "failed to exchange token: "+err.Error(), http.StatusUnauthorized)
|
}
|
||||||
// return
|
resp, err := rs.Introspect(r.Context(), provider, token)
|
||||||
// }
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
params := mux.Vars(r)
|
||||||
|
requestedClaim := params["claim"]
|
||||||
|
requestedValue := params["value"]
|
||||||
|
value, ok := resp.GetClaim(requestedClaim).(string)
|
||||||
|
if !ok || value == "" || value != requestedValue {
|
||||||
|
http.Error(w, "claim does not match", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Write([]byte("authorized with value " + value))
|
||||||
|
})
|
||||||
|
|
||||||
// data, err := json.Marshal(tokens)
|
lis := fmt.Sprintf("127.0.0.1:%s", port)
|
||||||
// if err != nil {
|
log.Printf("listening on http://%s/", lis)
|
||||||
// http.Error(w, err.Error(), http.StatusInternalServerError)
|
log.Fatal(http.ListenAndServe(lis, router))
|
||||||
// return
|
}
|
||||||
// }
|
|
||||||
// w.Write(data)
|
func checkToken(w http.ResponseWriter, r *http.Request) (bool, string) {
|
||||||
// })
|
auth := r.Header.Get("authorization")
|
||||||
|
if auth == "" {
|
||||||
// lis := fmt.Sprintf("127.0.0.1:%s", port)
|
http.Error(w, "auth header missing", http.StatusUnauthorized)
|
||||||
// log.Printf("listening on http://%s/", lis)
|
return false, ""
|
||||||
// log.Fatal(http.ListenAndServe(lis, nil))
|
}
|
||||||
// }
|
if !strings.HasPrefix(auth, oidc.PrefixBearer) {
|
||||||
|
http.Error(w, "invalid header", http.StatusUnauthorized)
|
||||||
// func checkToken(w http.ResponseWriter, r *http.Request) (bool, string) {
|
return false, ""
|
||||||
// token := r.Header.Get("authorization")
|
}
|
||||||
// if token == "" {
|
return true, strings.TrimPrefix(auth, oidc.PrefixBearer)
|
||||||
// http.Error(w, "Auth header missing", http.StatusUnauthorized)
|
|
||||||
// return false, ""
|
|
||||||
// }
|
|
||||||
// return true, token
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,8 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -14,8 +11,8 @@ import (
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"github.com/caos/oidc/pkg/client/rp"
|
||||||
"github.com/caos/oidc/pkg/oidc"
|
"github.com/caos/oidc/pkg/oidc"
|
||||||
"github.com/caos/oidc/pkg/rp"
|
|
||||||
"github.com/caos/oidc/pkg/utils"
|
"github.com/caos/oidc/pkg/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -27,18 +24,26 @@ var (
|
||||||
func main() {
|
func main() {
|
||||||
clientID := os.Getenv("CLIENT_ID")
|
clientID := os.Getenv("CLIENT_ID")
|
||||||
clientSecret := os.Getenv("CLIENT_SECRET")
|
clientSecret := os.Getenv("CLIENT_SECRET")
|
||||||
|
keyPath := os.Getenv("KEY_PATH")
|
||||||
issuer := os.Getenv("ISSUER")
|
issuer := os.Getenv("ISSUER")
|
||||||
port := os.Getenv("PORT")
|
port := os.Getenv("PORT")
|
||||||
scopes := strings.Split(os.Getenv("SCOPES"), " ")
|
scopes := strings.Split(os.Getenv("SCOPES"), " ")
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
redirectURI := fmt.Sprintf("http://localhost:%v%v", port, callbackPath)
|
redirectURI := fmt.Sprintf("http://localhost:%v%v", port, callbackPath)
|
||||||
cookieHandler := utils.NewCookieHandler(key, key, utils.WithUnsecure())
|
cookieHandler := utils.NewCookieHandler(key, key, utils.WithUnsecure())
|
||||||
provider, err := rp.NewRelayingPartyOIDC(issuer, clientID, clientSecret, redirectURI, scopes,
|
|
||||||
rp.WithPKCE(cookieHandler),
|
options := []rp.Option{
|
||||||
rp.WithVerifierOpts(rp.WithIssuedAtOffset(5*time.Second)),
|
rp.WithCookieHandler(cookieHandler),
|
||||||
)
|
rp.WithVerifierOpts(rp.WithIssuedAtOffset(5 * time.Second)),
|
||||||
|
}
|
||||||
|
if clientSecret == "" {
|
||||||
|
options = append(options, rp.WithPKCE(cookieHandler))
|
||||||
|
}
|
||||||
|
if keyPath != "" {
|
||||||
|
options = append(options, rp.WithClientKey(keyPath))
|
||||||
|
}
|
||||||
|
|
||||||
|
provider, err := rp.NewRelyingPartyOIDC(issuer, clientID, clientSecret, redirectURI, scopes, options...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatalf("error creating provider %s", err.Error())
|
logrus.Fatalf("error creating provider %s", err.Error())
|
||||||
}
|
}
|
||||||
|
@ -71,80 +76,6 @@ func main() {
|
||||||
//with the returned tokens from the token endpoint
|
//with the returned tokens from the token endpoint
|
||||||
http.Handle(callbackPath, rp.CodeExchangeHandler(marshal, provider))
|
http.Handle(callbackPath, rp.CodeExchangeHandler(marshal, provider))
|
||||||
|
|
||||||
http.HandleFunc("/test", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
tokens, err := rp.ClientCredentials(ctx, provider, "scope")
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "failed to exchange token: "+err.Error(), http.StatusUnauthorized)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
data, err := json.Marshal(tokens)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.Write(data)
|
|
||||||
})
|
|
||||||
|
|
||||||
http.HandleFunc("/jwt-profile", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if r.Method == "GET" {
|
|
||||||
tpl := `
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>Login</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<form method="POST" action="/jwt-profile" enctype="multipart/form-data">
|
|
||||||
<label for="key">Select a key file:</label>
|
|
||||||
<input type="file" accept=".json" id="key" name="key">
|
|
||||||
<button type="submit">Get Token</button>
|
|
||||||
</form>
|
|
||||||
</body>
|
|
||||||
</html>`
|
|
||||||
t, err := template.New("login").Parse(tpl)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
err = t.Execute(w, nil)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
err := r.ParseMultipartForm(4 << 10)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
file, handler, err := r.FormFile("key")
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
key, err := ioutil.ReadAll(file)
|
|
||||||
fmt.Println(handler.Header)
|
|
||||||
assertion, err := oidc.NewJWTProfileAssertionFromFileData(key, []string{issuer})
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
token, err := rp.JWTProfileAssertionExchange(ctx, assertion, scopes, provider)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
data, err := json.Marshal(token)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.Write(data)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
lis := fmt.Sprintf("127.0.0.1:%s", port)
|
lis := fmt.Sprintf("127.0.0.1:%s", port)
|
||||||
logrus.Infof("listening on http://%s/", lis)
|
logrus.Infof("listening on http://%s/", lis)
|
||||||
logrus.Fatal(http.ListenAndServe("127.0.0.1:"+port, nil))
|
logrus.Fatal(http.ListenAndServe("127.0.0.1:"+port, nil))
|
||||||
|
|
|
@ -10,8 +10,8 @@ import (
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
githubOAuth "golang.org/x/oauth2/github"
|
githubOAuth "golang.org/x/oauth2/github"
|
||||||
|
|
||||||
"github.com/caos/oidc/pkg/rp"
|
"github.com/caos/oidc/pkg/client/rp"
|
||||||
"github.com/caos/oidc/pkg/rp/cli"
|
"github.com/caos/oidc/pkg/client/rp/cli"
|
||||||
"github.com/caos/oidc/pkg/utils"
|
"github.com/caos/oidc/pkg/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -35,7 +35,7 @@ func main() {
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
cookieHandler := utils.NewCookieHandler(key, key, utils.WithUnsecure())
|
cookieHandler := utils.NewCookieHandler(key, key, utils.WithUnsecure())
|
||||||
relayingParty, err := rp.NewRelayingPartyOAuth(rpConfig, rp.WithCookieHandler(cookieHandler))
|
relyingParty, err := rp.NewRelyingPartyOAuth(rpConfig, rp.WithCookieHandler(cookieHandler))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("error creating relaying party: %v", err)
|
fmt.Printf("error creating relaying party: %v", err)
|
||||||
return
|
return
|
||||||
|
@ -43,9 +43,9 @@ func main() {
|
||||||
state := func() string {
|
state := func() string {
|
||||||
return uuid.New().String()
|
return uuid.New().String()
|
||||||
}
|
}
|
||||||
token := cli.CodeFlow(relayingParty, callbackPath, port, state)
|
token := cli.CodeFlow(relyingParty, callbackPath, port, state)
|
||||||
|
|
||||||
client := github.NewClient(relayingParty.OAuthConfig().Client(ctx, token.Token))
|
client := github.NewClient(relyingParty.OAuthConfig().Client(ctx, token.Token))
|
||||||
|
|
||||||
_, _, err = client.Users.Get(ctx, "")
|
_, _, err = client.Users.Get(ctx, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
180
example/client/service/service.go
Normal file
180
example/client/service/service.go
Normal file
|
@ -0,0 +1,180 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"html/template"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"golang.org/x/oauth2"
|
||||||
|
|
||||||
|
"github.com/caos/oidc/pkg/client/profile"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
client *http.Client = http.DefaultClient
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
keyPath := os.Getenv("KEY_PATH")
|
||||||
|
issuer := os.Getenv("ISSUER")
|
||||||
|
port := os.Getenv("PORT")
|
||||||
|
scopes := strings.Split(os.Getenv("SCOPES"), " ")
|
||||||
|
|
||||||
|
if keyPath != "" {
|
||||||
|
ts, err := profile.NewJWTProfileTokenSourceFromKeyFile(issuer, keyPath, scopes)
|
||||||
|
if err != nil {
|
||||||
|
logrus.Fatalf("error creating token source %s", err.Error())
|
||||||
|
}
|
||||||
|
client = oauth2.NewClient(context.Background(), ts)
|
||||||
|
}
|
||||||
|
|
||||||
|
http.HandleFunc("/jwt-profile", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method == "GET" {
|
||||||
|
tpl := `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Login</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<form method="POST" action="/jwt-profile" enctype="multipart/form-data">
|
||||||
|
<label for="key">Select a key file:</label>
|
||||||
|
<input type="file" accept=".json" id="key" name="key">
|
||||||
|
<button type="submit">Get Token</button>
|
||||||
|
</form>
|
||||||
|
</body>
|
||||||
|
</html>`
|
||||||
|
t, err := template.New("login").Parse(tpl)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = t.Execute(w, nil)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
err := r.ParseMultipartForm(4 << 10)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
file, _, err := r.FormFile("key")
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
key, err := ioutil.ReadAll(file)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ts, err := profile.NewJWTProfileTokenSourceFromKeyFileData(issuer, key, scopes)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
client = oauth2.NewClient(context.Background(), ts)
|
||||||
|
token, err := ts.Token()
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data, err := json.Marshal(token)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Write(data)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
http.HandleFunc("/test", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
tpl := `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Test</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<form method="POST" action="/test">
|
||||||
|
<label for="url">URL for test:</label>
|
||||||
|
<input type="text" id="url" name="url" width="200px">
|
||||||
|
<button type="submit">Test Token</button>
|
||||||
|
</form>
|
||||||
|
{{if .URL}}
|
||||||
|
<p>
|
||||||
|
Result for {{.URL}}: {{.Response}}
|
||||||
|
</p>
|
||||||
|
{{end}}
|
||||||
|
</body>
|
||||||
|
</html>`
|
||||||
|
err := r.ParseForm()
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
testURL := r.Form.Get("url")
|
||||||
|
var data struct {
|
||||||
|
URL string
|
||||||
|
Response interface{}
|
||||||
|
}
|
||||||
|
if testURL != "" {
|
||||||
|
data.URL = testURL
|
||||||
|
data.Response, err = callExampleEndpoint(client, testURL)
|
||||||
|
if err != nil {
|
||||||
|
data.Response = err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t, err := template.New("login").Parse(tpl)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = t.Execute(w, data)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
lis := fmt.Sprintf("127.0.0.1:%s", port)
|
||||||
|
logrus.Infof("listening on http://%s/", lis)
|
||||||
|
logrus.Fatal(http.ListenAndServe("127.0.0.1:"+port, nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
func callExampleEndpoint(client *http.Client, testURL string) (interface{}, error) {
|
||||||
|
req, err := http.NewRequest("GET", testURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := ioutil.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return "", fmt.Errorf("http status not ok: %s %s", resp.Status, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(resp.Header.Get("content-type"), "text/plain") {
|
||||||
|
return string(body), nil
|
||||||
|
}
|
||||||
|
return body, err
|
||||||
|
}
|
|
@ -184,22 +184,22 @@ func (s *AuthStorage) GetClientByClientID(_ context.Context, id string) (op.Clie
|
||||||
return nil, errors.New("not found")
|
return nil, errors.New("not found")
|
||||||
}
|
}
|
||||||
var appType op.ApplicationType
|
var appType op.ApplicationType
|
||||||
var authMethod op.AuthMethod
|
var authMethod oidc.AuthMethod
|
||||||
var accessTokenType op.AccessTokenType
|
var accessTokenType op.AccessTokenType
|
||||||
var responseTypes []oidc.ResponseType
|
var responseTypes []oidc.ResponseType
|
||||||
if id == "web" {
|
if id == "web" {
|
||||||
appType = op.ApplicationTypeWeb
|
appType = op.ApplicationTypeWeb
|
||||||
authMethod = op.AuthMethodBasic
|
authMethod = oidc.AuthMethodBasic
|
||||||
accessTokenType = op.AccessTokenTypeBearer
|
accessTokenType = op.AccessTokenTypeBearer
|
||||||
responseTypes = []oidc.ResponseType{oidc.ResponseTypeCode}
|
responseTypes = []oidc.ResponseType{oidc.ResponseTypeCode}
|
||||||
} else if id == "native" {
|
} else if id == "native" {
|
||||||
appType = op.ApplicationTypeNative
|
appType = op.ApplicationTypeNative
|
||||||
authMethod = op.AuthMethodNone
|
authMethod = oidc.AuthMethodNone
|
||||||
accessTokenType = op.AccessTokenTypeBearer
|
accessTokenType = op.AccessTokenTypeBearer
|
||||||
responseTypes = []oidc.ResponseType{oidc.ResponseTypeCode}
|
responseTypes = []oidc.ResponseType{oidc.ResponseTypeCode}
|
||||||
} else {
|
} else {
|
||||||
appType = op.ApplicationTypeUserAgent
|
appType = op.ApplicationTypeUserAgent
|
||||||
authMethod = op.AuthMethodNone
|
authMethod = oidc.AuthMethodNone
|
||||||
accessTokenType = op.AccessTokenTypeJWT
|
accessTokenType = op.AccessTokenTypeJWT
|
||||||
responseTypes = []oidc.ResponseType{oidc.ResponseTypeIDToken, oidc.ResponseTypeIDTokenOnly}
|
responseTypes = []oidc.ResponseType{oidc.ResponseTypeIDToken, oidc.ResponseTypeIDTokenOnly}
|
||||||
}
|
}
|
||||||
|
@ -210,26 +210,37 @@ func (s *AuthStorage) AuthorizeClientIDSecret(_ context.Context, id string, _ st
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *AuthStorage) GetUserinfoFromToken(ctx context.Context, _, _, _ string) (oidc.UserInfo, error) {
|
func (s *AuthStorage) SetUserinfoFromToken(ctx context.Context, userinfo oidc.UserInfoSetter, _, _, _ string) error {
|
||||||
return s.GetUserinfoFromScopes(ctx, "", "", []string{})
|
return s.SetUserinfoFromScopes(ctx, userinfo, "", "", []string{})
|
||||||
}
|
}
|
||||||
func (s *AuthStorage) GetUserinfoFromScopes(_ context.Context, _, _ string, _ []string) (oidc.UserInfo, error) {
|
func (s *AuthStorage) SetUserinfoFromScopes(ctx context.Context, userinfo oidc.UserInfoSetter, _, _ string, _ []string) error {
|
||||||
userinfo := oidc.NewUserInfo()
|
|
||||||
userinfo.SetSubject(a.GetSubject())
|
userinfo.SetSubject(a.GetSubject())
|
||||||
userinfo.SetAddress(oidc.NewUserInfoAddress("Test 789\nPostfach 2", "", "", "", "", ""))
|
userinfo.SetAddress(oidc.NewUserInfoAddress("Test 789\nPostfach 2", "", "", "", "", ""))
|
||||||
userinfo.SetEmail("test", true)
|
userinfo.SetEmail("test", true)
|
||||||
userinfo.SetPhone("0791234567", true)
|
userinfo.SetPhone("0791234567", true)
|
||||||
userinfo.SetName("Test")
|
userinfo.SetName("Test")
|
||||||
userinfo.AppendClaims("private_claim", "test")
|
userinfo.AppendClaims("private_claim", "test")
|
||||||
return userinfo, nil
|
return nil
|
||||||
}
|
}
|
||||||
func (s *AuthStorage) GetPrivateClaimsFromScopes(_ context.Context, _, _ string, _ []string) (map[string]interface{}, error) {
|
func (s *AuthStorage) GetPrivateClaimsFromScopes(_ context.Context, _, _ string, _ []string) (map[string]interface{}, error) {
|
||||||
return map[string]interface{}{"private_claim": "test"}, nil
|
return map[string]interface{}{"private_claim": "test"}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *AuthStorage) SetIntrospectionFromToken(ctx context.Context, introspect oidc.IntrospectionResponse, tokenID, subject, clientID string) error {
|
||||||
|
if err := s.SetUserinfoFromScopes(ctx, introspect, "", "", []string{}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
introspect.SetClientID(a.ClientID)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AuthStorage) ValidateJWTProfileScopes(ctx context.Context, userID string, scope oidc.Scopes) (oidc.Scopes, error) {
|
||||||
|
return scope, nil
|
||||||
|
}
|
||||||
|
|
||||||
type ConfClient struct {
|
type ConfClient struct {
|
||||||
applicationType op.ApplicationType
|
applicationType op.ApplicationType
|
||||||
authMethod op.AuthMethod
|
authMethod oidc.AuthMethod
|
||||||
responseTypes []oidc.ResponseType
|
responseTypes []oidc.ResponseType
|
||||||
ID string
|
ID string
|
||||||
accessTokenType op.AccessTokenType
|
accessTokenType op.AccessTokenType
|
||||||
|
@ -262,7 +273,7 @@ func (c *ConfClient) ApplicationType() op.ApplicationType {
|
||||||
return c.applicationType
|
return c.applicationType
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ConfClient) AuthMethod() op.AuthMethod {
|
func (c *ConfClient) AuthMethod() oidc.AuthMethod {
|
||||||
return c.authMethod
|
return c.authMethod
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
90
pkg/client/client.go
Normal file
90
pkg/client/client.go
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gorilla/schema"
|
||||||
|
"golang.org/x/oauth2"
|
||||||
|
"gopkg.in/square/go-jose.v2"
|
||||||
|
|
||||||
|
"github.com/caos/oidc/pkg/oidc"
|
||||||
|
"github.com/caos/oidc/pkg/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
Encoder = func() utils.Encoder {
|
||||||
|
e := schema.NewEncoder()
|
||||||
|
e.RegisterEncoder(oidc.Scopes{}, func(value reflect.Value) string {
|
||||||
|
return value.Interface().(oidc.Scopes).Encode()
|
||||||
|
})
|
||||||
|
return e
|
||||||
|
}()
|
||||||
|
)
|
||||||
|
|
||||||
|
//Discover calls the discovery endpoint of the provided issuer and returns its configuration
|
||||||
|
func Discover(issuer string, httpClient *http.Client) (*oidc.DiscoveryConfiguration, error) {
|
||||||
|
wellKnown := strings.TrimSuffix(issuer, "/") + oidc.DiscoveryEndpoint
|
||||||
|
req, err := http.NewRequest("GET", wellKnown, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
discoveryConfig := new(oidc.DiscoveryConfiguration)
|
||||||
|
err = utils.HttpRequest(httpClient, req, &discoveryConfig)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return discoveryConfig, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type tokenEndpointCaller interface {
|
||||||
|
TokenEndpoint() string
|
||||||
|
HttpClient() *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func CallTokenEndpoint(request interface{}, caller tokenEndpointCaller) (newToken *oauth2.Token, err error) {
|
||||||
|
return callTokenEndpoint(request, nil, caller)
|
||||||
|
}
|
||||||
|
|
||||||
|
func callTokenEndpoint(request interface{}, authFn interface{}, caller tokenEndpointCaller) (newToken *oauth2.Token, err error) {
|
||||||
|
req, err := utils.FormRequest(caller.TokenEndpoint(), request, Encoder, authFn)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
tokenRes := new(oidc.AccessTokenResponse)
|
||||||
|
if err := utils.HttpRequest(caller.HttpClient(), req, &tokenRes); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &oauth2.Token{
|
||||||
|
AccessToken: tokenRes.AccessToken,
|
||||||
|
TokenType: tokenRes.TokenType,
|
||||||
|
RefreshToken: tokenRes.RefreshToken,
|
||||||
|
Expiry: time.Now().UTC().Add(time.Duration(tokenRes.ExpiresIn) * time.Second),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSignerFromPrivateKeyByte(key []byte, keyID string) (jose.Signer, error) {
|
||||||
|
privateKey, err := utils.BytesToPrivateKey(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
signingKey := jose.SigningKey{
|
||||||
|
Algorithm: jose.RS256,
|
||||||
|
Key: &jose.JSONWebKey{Key: privateKey, KeyID: keyID},
|
||||||
|
}
|
||||||
|
return jose.NewSigner(signingKey, &jose.SignerOptions{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func SignedJWTProfileAssertion(clientID string, audience []string, expiration time.Duration, signer jose.Signer) (string, error) {
|
||||||
|
iat := time.Now()
|
||||||
|
exp := iat.Add(expiration)
|
||||||
|
return utils.Sign(&oidc.JWTTokenRequest{
|
||||||
|
Issuer: clientID,
|
||||||
|
Subject: clientID,
|
||||||
|
Audience: audience,
|
||||||
|
ExpiresAt: oidc.Time(exp),
|
||||||
|
IssuedAt: oidc.Time(iat),
|
||||||
|
}, signer)
|
||||||
|
}
|
30
pkg/client/jwt_profile.go
Normal file
30
pkg/client/jwt_profile.go
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"golang.org/x/oauth2"
|
||||||
|
|
||||||
|
"github.com/caos/oidc/pkg/oidc"
|
||||||
|
"github.com/caos/oidc/pkg/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
//JWTProfileExchange handles the oauth2 jwt profile exchange
|
||||||
|
func JWTProfileExchange(ctx context.Context, jwtProfileGrantRequest *oidc.JWTProfileGrantRequest, caller tokenEndpointCaller) (*oauth2.Token, error) {
|
||||||
|
return CallTokenEndpoint(jwtProfileGrantRequest, caller)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ClientAssertionCodeOptions(assertion string) []oauth2.AuthCodeOption {
|
||||||
|
return []oauth2.AuthCodeOption{
|
||||||
|
oauth2.SetAuthURLParam("client_assertion", assertion),
|
||||||
|
oauth2.SetAuthURLParam("client_assertion_type", oidc.ClientAssertionTypeJWTAssertion),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ClientAssertionFormAuthorization(assertion string) utils.FormAuthorization {
|
||||||
|
return func(values url.Values) {
|
||||||
|
values.Set("client_assertion", assertion)
|
||||||
|
values.Set("client_assertion_type", oidc.ClientAssertionTypeJWTAssertion)
|
||||||
|
}
|
||||||
|
}
|
40
pkg/client/key.go
Normal file
40
pkg/client/key.go
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"io/ioutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
serviceAccountKey = "serviceaccount"
|
||||||
|
applicationKey = "application"
|
||||||
|
)
|
||||||
|
|
||||||
|
type keyFile struct {
|
||||||
|
Type string `json:"type"` // serviceaccount or application
|
||||||
|
KeyID string `json:"keyId"`
|
||||||
|
Key string `json:"key"`
|
||||||
|
Issuer string `json:"issuer"` //not yet in file
|
||||||
|
|
||||||
|
//serviceaccount
|
||||||
|
UserID string `json:"userId"`
|
||||||
|
|
||||||
|
//application
|
||||||
|
ClientID string `json:"clientId"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func ConfigFromKeyFile(path string) (*keyFile, error) {
|
||||||
|
data, err := ioutil.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return ConfigFromKeyFileData(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ConfigFromKeyFileData(data []byte) (*keyFile, error) {
|
||||||
|
var f keyFile
|
||||||
|
if err := json.Unmarshal(data, &f); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &f, nil
|
||||||
|
}
|
93
pkg/client/profile/jwt_profile.go
Normal file
93
pkg/client/profile/jwt_profile.go
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
package profile
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/oauth2"
|
||||||
|
"gopkg.in/square/go-jose.v2"
|
||||||
|
|
||||||
|
"github.com/caos/oidc/pkg/client"
|
||||||
|
"github.com/caos/oidc/pkg/oidc"
|
||||||
|
)
|
||||||
|
|
||||||
|
//jwtProfileTokenSource implement the oauth2.TokenSource
|
||||||
|
//it will request a token using the OAuth2 JWT Profile Grant
|
||||||
|
//therefore sending an `assertion` by singing a JWT with the provided private key
|
||||||
|
type jwtProfileTokenSource struct {
|
||||||
|
clientID string
|
||||||
|
audience []string
|
||||||
|
signer jose.Signer
|
||||||
|
scopes []string
|
||||||
|
httpClient *http.Client
|
||||||
|
tokenEndpoint string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewJWTProfileTokenSourceFromKeyFile(issuer, keyPath string, scopes []string, options ...func(source *jwtProfileTokenSource)) (oauth2.TokenSource, error) {
|
||||||
|
keyData, err := client.ConfigFromKeyFile(keyPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return NewJWTProfileTokenSource(issuer, keyData.UserID, keyData.KeyID, []byte(keyData.Key), scopes, options...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewJWTProfileTokenSourceFromKeyFileData(issuer string, data []byte, scopes []string, options ...func(source *jwtProfileTokenSource)) (oauth2.TokenSource, error) {
|
||||||
|
keyData, err := client.ConfigFromKeyFileData(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return NewJWTProfileTokenSource(issuer, keyData.UserID, keyData.KeyID, []byte(keyData.Key), scopes, options...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewJWTProfileTokenSource(issuer, clientID, keyID string, key []byte, scopes []string, options ...func(source *jwtProfileTokenSource)) (oauth2.TokenSource, error) {
|
||||||
|
signer, err := client.NewSignerFromPrivateKeyByte(key, keyID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
source := &jwtProfileTokenSource{
|
||||||
|
clientID: clientID,
|
||||||
|
audience: []string{issuer},
|
||||||
|
signer: signer,
|
||||||
|
scopes: scopes,
|
||||||
|
httpClient: http.DefaultClient,
|
||||||
|
}
|
||||||
|
for _, opt := range options {
|
||||||
|
opt(source)
|
||||||
|
}
|
||||||
|
if source.tokenEndpoint == "" {
|
||||||
|
config, err := client.Discover(issuer, source.httpClient)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
source.tokenEndpoint = config.TokenEndpoint
|
||||||
|
}
|
||||||
|
return source, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithHTTPClient(client *http.Client) func(*jwtProfileTokenSource) {
|
||||||
|
return func(source *jwtProfileTokenSource) {
|
||||||
|
source.httpClient = client
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithStaticTokenEndpoint(issuer, tokenEndpoint string) func(*jwtProfileTokenSource) {
|
||||||
|
return func(source *jwtProfileTokenSource) {
|
||||||
|
source.tokenEndpoint = tokenEndpoint
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *jwtProfileTokenSource) TokenEndpoint() string {
|
||||||
|
return j.tokenEndpoint
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *jwtProfileTokenSource) HttpClient() *http.Client {
|
||||||
|
return j.httpClient
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *jwtProfileTokenSource) Token() (*oauth2.Token, error) {
|
||||||
|
assertion, err := client.SignedJWTProfileAssertion(j.clientID, j.audience, time.Hour, j.signer)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return client.JWTProfileExchange(nil, oidc.NewJWTProfileGrantRequest(assertion, j.scopes...), j)
|
||||||
|
}
|
|
@ -4,8 +4,8 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/caos/oidc/pkg/client/rp"
|
||||||
"github.com/caos/oidc/pkg/oidc"
|
"github.com/caos/oidc/pkg/oidc"
|
||||||
"github.com/caos/oidc/pkg/rp"
|
|
||||||
"github.com/caos/oidc/pkg/utils"
|
"github.com/caos/oidc/pkg/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -13,7 +13,7 @@ const (
|
||||||
loginPath = "/login"
|
loginPath = "/login"
|
||||||
)
|
)
|
||||||
|
|
||||||
func CodeFlow(relayingParty rp.RelayingParty, callbackPath, port string, stateProvider func() string) *oidc.Tokens {
|
func CodeFlow(relyingParty rp.RelyingParty, callbackPath, port string, stateProvider func() string) *oidc.Tokens {
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
|
@ -24,8 +24,8 @@ func CodeFlow(relayingParty rp.RelayingParty, callbackPath, port string, statePr
|
||||||
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>"
|
||||||
w.Write([]byte(msg))
|
w.Write([]byte(msg))
|
||||||
}
|
}
|
||||||
http.Handle(loginPath, rp.AuthURLHandler(stateProvider, relayingParty))
|
http.Handle(loginPath, rp.AuthURLHandler(stateProvider, relyingParty))
|
||||||
http.Handle(callbackPath, rp.CodeExchangeHandler(callback, relayingParty))
|
http.Handle(callbackPath, rp.CodeExchangeHandler(callback, relyingParty))
|
||||||
|
|
||||||
utils.StartServer(ctx, port)
|
utils.StartServer(ctx, port)
|
||||||
|
|
|
@ -5,10 +5,12 @@
|
||||||
package mock
|
package mock
|
||||||
|
|
||||||
import (
|
import (
|
||||||
context "context"
|
"context"
|
||||||
oidc "github.com/caos/oidc/pkg/oidc"
|
"reflect"
|
||||||
gomock "github.com/golang/mock/gomock"
|
|
||||||
reflect "reflect"
|
"github.com/golang/mock/gomock"
|
||||||
|
|
||||||
|
"github.com/caos/oidc/pkg/oidc"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MockVerifier is a mock of Verifier interface
|
// MockVerifier is a mock of Verifier interface
|
|
@ -4,43 +4,32 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
|
||||||
"reflect"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/gorilla/schema"
|
|
||||||
|
|
||||||
"github.com/caos/oidc/pkg/oidc"
|
|
||||||
"github.com/caos/oidc/pkg/oidc/grants"
|
|
||||||
"github.com/caos/oidc/pkg/utils"
|
|
||||||
|
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
|
"gopkg.in/square/go-jose.v2"
|
||||||
|
|
||||||
|
"github.com/caos/oidc/pkg/client"
|
||||||
|
"github.com/caos/oidc/pkg/oidc"
|
||||||
|
"github.com/caos/oidc/pkg/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
idTokenKey = "id_token"
|
idTokenKey = "id_token"
|
||||||
stateParam = "state"
|
stateParam = "state"
|
||||||
pkceCode = "pkce"
|
pkceCode = "pkce"
|
||||||
jwtProfileKey = "urn:ietf:params:oauth:grant-type:jwt-bearer"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
//RelyingParty declares the minimal interface for oidc clients
|
||||||
encoder = func() utils.Encoder {
|
type RelyingParty interface {
|
||||||
e := schema.NewEncoder()
|
|
||||||
e.RegisterEncoder(oidc.Scopes{}, func(value reflect.Value) string {
|
|
||||||
return value.Interface().(oidc.Scopes).Encode()
|
|
||||||
})
|
|
||||||
return e
|
|
||||||
}()
|
|
||||||
)
|
|
||||||
|
|
||||||
//RelayingParty declares the minimal interface for oidc clients
|
|
||||||
type RelayingParty interface {
|
|
||||||
//OAuthConfig returns the oauth2 Config
|
//OAuthConfig returns the oauth2 Config
|
||||||
OAuthConfig() *oauth2.Config
|
OAuthConfig() *oauth2.Config
|
||||||
|
|
||||||
|
//Issuer returns the issuer of the oidc config
|
||||||
|
Issuer() string
|
||||||
|
|
||||||
//IsPKCE returns if authorization is done using `Authorization Code Flow with Proof Key for Code Exchange (PKCE)`
|
//IsPKCE returns if authorization is done using `Authorization Code Flow with Proof Key for Code Exchange (PKCE)`
|
||||||
IsPKCE() bool
|
IsPKCE() bool
|
||||||
|
|
||||||
|
@ -53,10 +42,16 @@ type RelayingParty interface {
|
||||||
//IsOAuth2Only specifies whether relaying party handles only oauth2 or oidc calls
|
//IsOAuth2Only specifies whether relaying party handles only oauth2 or oidc calls
|
||||||
IsOAuth2Only() bool
|
IsOAuth2Only() bool
|
||||||
|
|
||||||
|
//Signer is used if the relaying party uses the JWT Profile
|
||||||
|
Signer() jose.Signer
|
||||||
|
|
||||||
|
//UserinfoEndpoint returns the userinfo
|
||||||
|
UserinfoEndpoint() 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
|
||||||
|
|
||||||
ErrorHandler() func(http.ResponseWriter, *http.Request, string, string, string)
|
ErrorHandler() func(http.ResponseWriter, *http.Request, string, string, string)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,7 +63,7 @@ var (
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
type relayingParty struct {
|
type relyingParty struct {
|
||||||
issuer string
|
issuer string
|
||||||
endpoints Endpoints
|
endpoints Endpoints
|
||||||
oauthConfig *oauth2.Config
|
oauthConfig *oauth2.Config
|
||||||
|
@ -77,68 +72,83 @@ type relayingParty struct {
|
||||||
|
|
||||||
httpClient *http.Client
|
httpClient *http.Client
|
||||||
cookieHandler *utils.CookieHandler
|
cookieHandler *utils.CookieHandler
|
||||||
errorHandler func(http.ResponseWriter, *http.Request, string, string, string)
|
|
||||||
|
|
||||||
|
errorHandler func(http.ResponseWriter, *http.Request, string, string, string)
|
||||||
idTokenVerifier IDTokenVerifier
|
idTokenVerifier IDTokenVerifier
|
||||||
verifierOpts []VerifierOption
|
verifierOpts []VerifierOption
|
||||||
|
signer jose.Signer
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rp *relayingParty) OAuthConfig() *oauth2.Config {
|
func (rp *relyingParty) OAuthConfig() *oauth2.Config {
|
||||||
return rp.oauthConfig
|
return rp.oauthConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rp *relayingParty) IsPKCE() bool {
|
func (rp *relyingParty) Issuer() string {
|
||||||
|
return rp.issuer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rp *relyingParty) IsPKCE() bool {
|
||||||
return rp.pkce
|
return rp.pkce
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rp *relayingParty) CookieHandler() *utils.CookieHandler {
|
func (rp *relyingParty) CookieHandler() *utils.CookieHandler {
|
||||||
return rp.cookieHandler
|
return rp.cookieHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rp *relayingParty) HttpClient() *http.Client {
|
func (rp *relyingParty) HttpClient() *http.Client {
|
||||||
return rp.httpClient
|
return rp.httpClient
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rp *relayingParty) IsOAuth2Only() bool {
|
func (rp *relyingParty) IsOAuth2Only() bool {
|
||||||
return rp.oauth2Only
|
return rp.oauth2Only
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rp *relayingParty) IDTokenVerifier() IDTokenVerifier {
|
func (rp *relyingParty) Signer() jose.Signer {
|
||||||
|
return rp.signer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rp *relyingParty) UserinfoEndpoint() string {
|
||||||
|
return rp.endpoints.UserinfoURL
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rp *relyingParty) IDTokenVerifier() IDTokenVerifier {
|
||||||
if rp.idTokenVerifier == nil {
|
if rp.idTokenVerifier == nil {
|
||||||
rp.idTokenVerifier = NewIDTokenVerifier(rp.issuer, rp.oauthConfig.ClientID, NewRemoteKeySet(rp.httpClient, rp.endpoints.JKWsURL), rp.verifierOpts...)
|
rp.idTokenVerifier = NewIDTokenVerifier(rp.issuer, rp.oauthConfig.ClientID, NewRemoteKeySet(rp.httpClient, rp.endpoints.JKWsURL), rp.verifierOpts...)
|
||||||
}
|
}
|
||||||
return rp.idTokenVerifier
|
return rp.idTokenVerifier
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rp *relayingParty) ErrorHandler() func(http.ResponseWriter, *http.Request, string, string, string) {
|
func (rp *relyingParty) ErrorHandler() func(http.ResponseWriter, *http.Request, string, string, string) {
|
||||||
if rp.errorHandler == nil {
|
if rp.errorHandler == nil {
|
||||||
rp.errorHandler = DefaultErrorHandler
|
rp.errorHandler = DefaultErrorHandler
|
||||||
}
|
}
|
||||||
return rp.errorHandler
|
return rp.errorHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
//NewRelayingPartyOAuth creates an (OAuth2) RelayingParty with the given
|
//NewRelyingPartyOAuth creates an (OAuth2) RelyingParty with the given
|
||||||
//OAuth2 Config and possible configOptions
|
//OAuth2 Config and possible configOptions
|
||||||
//it will use the AuthURL and TokenURL set in config
|
//it will use the AuthURL and TokenURL set in config
|
||||||
func NewRelayingPartyOAuth(config *oauth2.Config, options ...Option) (RelayingParty, error) {
|
func NewRelyingPartyOAuth(config *oauth2.Config, options ...Option) (RelyingParty, error) {
|
||||||
rp := &relayingParty{
|
rp := &relyingParty{
|
||||||
oauthConfig: config,
|
oauthConfig: config,
|
||||||
httpClient: utils.DefaultHTTPClient,
|
httpClient: utils.DefaultHTTPClient,
|
||||||
oauth2Only: true,
|
oauth2Only: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, optFunc := range options {
|
for _, optFunc := range options {
|
||||||
optFunc(rp)
|
if err := optFunc(rp); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return rp, nil
|
return rp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
//NewRelayingPartyOIDC creates an (OIDC) RelayingParty with the given
|
//NewRelyingPartyOIDC creates an (OIDC) RelyingParty with the given
|
||||||
//issuer, clientID, clientSecret, redirectURI, scopes and possible configOptions
|
//issuer, clientID, clientSecret, redirectURI, scopes and possible configOptions
|
||||||
//it will run discovery on the provided issuer and use the found endpoints
|
//it will run discovery on the provided issuer and use the found endpoints
|
||||||
func NewRelayingPartyOIDC(issuer, clientID, clientSecret, redirectURI string, scopes []string, options ...Option) (RelayingParty, error) {
|
func NewRelyingPartyOIDC(issuer, clientID, clientSecret, redirectURI string, scopes []string, options ...Option) (RelyingParty, error) {
|
||||||
rp := &relayingParty{
|
rp := &relyingParty{
|
||||||
issuer: issuer,
|
issuer: issuer,
|
||||||
oauthConfig: &oauth2.Config{
|
oauthConfig: &oauth2.Config{
|
||||||
ClientID: clientID,
|
ClientID: clientID,
|
||||||
|
@ -151,7 +161,9 @@ func NewRelayingPartyOIDC(issuer, clientID, clientSecret, redirectURI string, sc
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, optFunc := range options {
|
for _, optFunc := range options {
|
||||||
optFunc(rp)
|
if err := optFunc(rp); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
endpoints, err := Discover(rp.issuer, rp.httpClient)
|
endpoints, err := Discover(rp.issuer, rp.httpClient)
|
||||||
|
@ -165,12 +177,13 @@ func NewRelayingPartyOIDC(issuer, clientID, clientSecret, redirectURI string, sc
|
||||||
}
|
}
|
||||||
|
|
||||||
//DefaultRPOpts is the type for providing dynamic options to the DefaultRP
|
//DefaultRPOpts is the type for providing dynamic options to the DefaultRP
|
||||||
type Option func(*relayingParty)
|
type Option func(*relyingParty) error
|
||||||
|
|
||||||
//WithCookieHandler set a `CookieHandler` for securing the various redirects
|
//WithCookieHandler set a `CookieHandler` for securing the various redirects
|
||||||
func WithCookieHandler(cookieHandler *utils.CookieHandler) Option {
|
func WithCookieHandler(cookieHandler *utils.CookieHandler) Option {
|
||||||
return func(rp *relayingParty) {
|
return func(rp *relyingParty) error {
|
||||||
rp.cookieHandler = cookieHandler
|
rp.cookieHandler = cookieHandler
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -178,32 +191,49 @@ func WithCookieHandler(cookieHandler *utils.CookieHandler) Option {
|
||||||
//it also sets a `CookieHandler` for securing the various redirects
|
//it also sets a `CookieHandler` for securing the various redirects
|
||||||
//and exchanging the code challenge
|
//and exchanging the code challenge
|
||||||
func WithPKCE(cookieHandler *utils.CookieHandler) Option {
|
func WithPKCE(cookieHandler *utils.CookieHandler) Option {
|
||||||
return func(rp *relayingParty) {
|
return func(rp *relyingParty) error {
|
||||||
rp.pkce = true
|
rp.pkce = true
|
||||||
rp.cookieHandler = cookieHandler
|
rp.cookieHandler = cookieHandler
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//WithHTTPClient provides the ability to set an http client to be used for the relaying party and verifier
|
//WithHTTPClient provides the ability to set an http client to be used for the relaying party and verifier
|
||||||
func WithHTTPClient(client *http.Client) Option {
|
func WithHTTPClient(client *http.Client) Option {
|
||||||
return func(rp *relayingParty) {
|
return func(rp *relyingParty) error {
|
||||||
rp.httpClient = client
|
rp.httpClient = client
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func WithErrorHandler(errorHandler ErrorHandler) Option {
|
func WithErrorHandler(errorHandler ErrorHandler) Option {
|
||||||
return func(rp *relayingParty) {
|
return func(rp *relyingParty) error {
|
||||||
rp.errorHandler = errorHandler
|
rp.errorHandler = errorHandler
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func WithVerifierOpts(opts ...VerifierOption) Option {
|
func WithVerifierOpts(opts ...VerifierOption) Option {
|
||||||
return func(rp *relayingParty) {
|
return func(rp *relyingParty) error {
|
||||||
rp.verifierOpts = opts
|
rp.verifierOpts = opts
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithClientKey(path string) Option {
|
||||||
|
return func(rp *relyingParty) error {
|
||||||
|
config, err := client.ConfigFromKeyFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
rp.signer, err = client.NewSignerFromPrivateKeyByte([]byte(config.Key), config.KeyID)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//Discover calls the discovery endpoint of the provided issuer and returns the found endpoints
|
//Discover calls the discovery endpoint of the provided issuer and returns the found endpoints
|
||||||
|
//
|
||||||
|
//deprecated: use client.Discover
|
||||||
func Discover(issuer string, httpClient *http.Client) (Endpoints, error) {
|
func Discover(issuer string, httpClient *http.Client) (Endpoints, error) {
|
||||||
wellKnown := strings.TrimSuffix(issuer, "/") + oidc.DiscoveryEndpoint
|
wellKnown := strings.TrimSuffix(issuer, "/") + oidc.DiscoveryEndpoint
|
||||||
req, err := http.NewRequest("GET", wellKnown, nil)
|
req, err := http.NewRequest("GET", wellKnown, nil)
|
||||||
|
@ -220,7 +250,7 @@ func Discover(issuer string, httpClient *http.Client) (Endpoints, error) {
|
||||||
|
|
||||||
//AuthURL returns the auth request url
|
//AuthURL returns the auth request url
|
||||||
//(wrapping the oauth2 `AuthCodeURL`)
|
//(wrapping the oauth2 `AuthCodeURL`)
|
||||||
func AuthURL(state string, rp RelayingParty, opts ...AuthURLOpt) string {
|
func AuthURL(state string, rp RelyingParty, opts ...AuthURLOpt) string {
|
||||||
authOpts := make([]oauth2.AuthCodeOption, 0)
|
authOpts := make([]oauth2.AuthCodeOption, 0)
|
||||||
for _, opt := range opts {
|
for _, opt := range opts {
|
||||||
authOpts = append(authOpts, opt()...)
|
authOpts = append(authOpts, opt()...)
|
||||||
|
@ -230,7 +260,7 @@ func AuthURL(state string, rp RelayingParty, opts ...AuthURLOpt) string {
|
||||||
|
|
||||||
//AuthURLHandler extends the `AuthURL` method with a http redirect handler
|
//AuthURLHandler extends the `AuthURL` method with a http redirect handler
|
||||||
//including handling setting cookie for secure `state` transfer
|
//including handling setting cookie for secure `state` transfer
|
||||||
func AuthURLHandler(stateFn func() string, rp RelayingParty) http.HandlerFunc {
|
func AuthURLHandler(stateFn func() string, rp RelyingParty) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
opts := make([]AuthURLOpt, 0)
|
opts := make([]AuthURLOpt, 0)
|
||||||
state := stateFn()
|
state := stateFn()
|
||||||
|
@ -251,7 +281,7 @@ func AuthURLHandler(stateFn func() string, rp RelayingParty) http.HandlerFunc {
|
||||||
}
|
}
|
||||||
|
|
||||||
//GenerateAndStoreCodeChallenge generates a PKCE code challenge and stores its verifier into a secure cookie
|
//GenerateAndStoreCodeChallenge generates a PKCE code challenge and stores its verifier into a secure cookie
|
||||||
func GenerateAndStoreCodeChallenge(w http.ResponseWriter, rp RelayingParty) (string, error) {
|
func GenerateAndStoreCodeChallenge(w http.ResponseWriter, rp RelyingParty) (string, error) {
|
||||||
codeVerifier := uuid.New().String()
|
codeVerifier := uuid.New().String()
|
||||||
if err := rp.CookieHandler().SetCookie(w, pkceCode, codeVerifier); err != nil {
|
if err := rp.CookieHandler().SetCookie(w, pkceCode, codeVerifier); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
|
@ -261,7 +291,7 @@ func GenerateAndStoreCodeChallenge(w http.ResponseWriter, rp RelayingParty) (str
|
||||||
|
|
||||||
//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 RelayingParty, opts ...CodeExchangeOpt) (tokens *oidc.Tokens, err error) {
|
func CodeExchange(ctx context.Context, code string, rp RelyingParty, opts ...CodeExchangeOpt) (tokens *oidc.Tokens, 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 {
|
||||||
|
@ -293,7 +323,7 @@ func CodeExchange(ctx context.Context, code string, rp RelayingParty, opts ...Co
|
||||||
//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
|
||||||
func CodeExchangeHandler(callback func(http.ResponseWriter, *http.Request, *oidc.Tokens, string), rp RelayingParty) http.HandlerFunc {
|
func CodeExchangeHandler(callback func(http.ResponseWriter, *http.Request, *oidc.Tokens, string), rp RelyingParty) 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 {
|
||||||
|
@ -314,6 +344,14 @@ func CodeExchangeHandler(callback func(http.ResponseWriter, *http.Request, *oidc
|
||||||
}
|
}
|
||||||
codeOpts = append(codeOpts, WithCodeVerifier(codeVerifier))
|
codeOpts = append(codeOpts, WithCodeVerifier(codeVerifier))
|
||||||
}
|
}
|
||||||
|
if rp.Signer() != nil {
|
||||||
|
assertion, err := client.SignedJWTProfileAssertion(rp.OAuthConfig().ClientID, []string{rp.Issuer()}, time.Hour, rp.Signer())
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "failed to build assertion: "+err.Error(), http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
codeOpts = append(codeOpts, WithClientAssertionJWT(assertion))
|
||||||
|
}
|
||||||
tokens, err := CodeExchange(r.Context(), params.Get("code"), rp, codeOpts...)
|
tokens, err := CodeExchange(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)
|
||||||
|
@ -323,51 +361,21 @@ func CodeExchangeHandler(callback func(http.ResponseWriter, *http.Request, *oidc
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//ClientCredentials is the `RelayingParty` interface implementation
|
//Userinfo will call the OIDC Userinfo Endpoint with the provided token
|
||||||
//handling the oauth2 client credentials grant
|
func Userinfo(token string, rp RelyingParty) (oidc.UserInfo, error) {
|
||||||
func ClientCredentials(ctx context.Context, rp RelayingParty, scopes ...string) (newToken *oauth2.Token, err error) {
|
req, err := http.NewRequest("GET", rp.UserinfoEndpoint(), nil)
|
||||||
return CallTokenEndpointAuthorized(grants.ClientCredentialsGrantBasic(scopes...), rp)
|
|
||||||
}
|
|
||||||
|
|
||||||
func CallTokenEndpointAuthorized(request interface{}, rp RelayingParty) (newToken *oauth2.Token, err error) {
|
|
||||||
config := rp.OAuthConfig()
|
|
||||||
var fn interface{} = utils.AuthorizeBasic(config.ClientID, config.ClientSecret)
|
|
||||||
if config.Endpoint.AuthStyle == oauth2.AuthStyleInParams {
|
|
||||||
fn = func(form url.Values) {
|
|
||||||
form.Set("client_id", config.ClientID)
|
|
||||||
form.Set("client_secret", config.ClientSecret)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return callTokenEndpoint(request, fn, rp)
|
|
||||||
}
|
|
||||||
|
|
||||||
func CallTokenEndpoint(request interface{}, rp RelayingParty) (newToken *oauth2.Token, err error) {
|
|
||||||
return callTokenEndpoint(request, nil, rp)
|
|
||||||
}
|
|
||||||
|
|
||||||
func callTokenEndpoint(request interface{}, authFn interface{}, rp RelayingParty) (newToken *oauth2.Token, err error) {
|
|
||||||
req, err := utils.FormRequest(rp.OAuthConfig().Endpoint.TokenURL, request, encoder, authFn)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
var tokenRes struct {
|
req.Header.Set("authorization", token)
|
||||||
AccessToken string `json:"access_token"`
|
userinfo := oidc.NewUserInfo()
|
||||||
TokenType string `json:"token_type"`
|
if err := utils.HttpRequest(rp.HttpClient(), req, &userinfo); err != nil {
|
||||||
ExpiresIn int64 `json:"expires_in"`
|
|
||||||
RefreshToken string `json:"refresh_token"`
|
|
||||||
}
|
|
||||||
if err := utils.HttpRequest(rp.HttpClient(), req, &tokenRes); err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &oauth2.Token{
|
return userinfo, nil
|
||||||
AccessToken: tokenRes.AccessToken,
|
|
||||||
TokenType: tokenRes.TokenType,
|
|
||||||
RefreshToken: tokenRes.RefreshToken,
|
|
||||||
Expiry: time.Now().UTC().Add(time.Duration(tokenRes.ExpiresIn) * time.Second),
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func trySetStateCookie(w http.ResponseWriter, state string, rp RelayingParty) error {
|
func trySetStateCookie(w http.ResponseWriter, state string, rp RelyingParty) error {
|
||||||
if rp.CookieHandler() != nil {
|
if rp.CookieHandler() != nil {
|
||||||
if err := rp.CookieHandler().SetCookie(w, stateParam, state); err != nil {
|
if err := rp.CookieHandler().SetCookie(w, stateParam, state); err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -376,7 +384,7 @@ func trySetStateCookie(w http.ResponseWriter, state string, rp RelayingParty) er
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func tryReadStateCookie(w http.ResponseWriter, r *http.Request, rp RelayingParty) (state string, err error) {
|
func tryReadStateCookie(w http.ResponseWriter, r *http.Request, rp RelyingParty) (state string, err error) {
|
||||||
if rp.CookieHandler() == nil {
|
if rp.CookieHandler() == nil {
|
||||||
return r.FormValue(stateParam), nil
|
return r.FormValue(stateParam), nil
|
||||||
}
|
}
|
||||||
|
@ -388,7 +396,7 @@ func tryReadStateCookie(w http.ResponseWriter, r *http.Request, rp RelayingParty
|
||||||
return state, nil
|
return state, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type OptionFunc func(RelayingParty)
|
type OptionFunc func(RelyingParty)
|
||||||
|
|
||||||
type Endpoints struct {
|
type Endpoints struct {
|
||||||
oauth2.Endpoint
|
oauth2.Endpoint
|
||||||
|
@ -439,3 +447,10 @@ func WithCodeVerifier(codeVerifier string) CodeExchangeOpt {
|
||||||
return []oauth2.AuthCodeOption{oauth2.SetAuthURLParam("code_verifier", codeVerifier)}
|
return []oauth2.AuthCodeOption{oauth2.SetAuthURLParam("code_verifier", codeVerifier)}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//WithClientAssertionJWT sets the `client_assertion` param in the token request
|
||||||
|
func WithClientAssertionJWT(clientAssertion string) CodeExchangeOpt {
|
||||||
|
return func() []oauth2.AuthCodeOption {
|
||||||
|
return client.ClientAssertionCodeOptions(clientAssertion)
|
||||||
|
}
|
||||||
|
}
|
27
pkg/client/rp/tockenexchange.go
Normal file
27
pkg/client/rp/tockenexchange.go
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
package rp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"golang.org/x/oauth2"
|
||||||
|
|
||||||
|
"github.com/caos/oidc/pkg/oidc/grants/tokenexchange"
|
||||||
|
)
|
||||||
|
|
||||||
|
//TokenExchangeRP extends the `RelyingParty` interface for the *draft* oauth2 `Token Exchange`
|
||||||
|
type TokenExchangeRP interface {
|
||||||
|
RelyingParty
|
||||||
|
|
||||||
|
//TokenExchange implement the `Token Exchange Grant` exchanging some token for an other
|
||||||
|
TokenExchange(context.Context, *tokenexchange.TokenExchangeRequest) (*oauth2.Token, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
//DelegationTokenExchangeRP extends the `TokenExchangeRP` interface
|
||||||
|
//for the specific `delegation token` request
|
||||||
|
type DelegationTokenExchangeRP interface {
|
||||||
|
TokenExchangeRP
|
||||||
|
|
||||||
|
//DelegationTokenExchange implement the `Token Exchange Grant`
|
||||||
|
//providing an access token in request for a `delegation` token for a given resource / audience
|
||||||
|
DelegationTokenExchange(context.Context, string, ...tokenexchange.TokenExchangeOption) (*oauth2.Token, error)
|
||||||
|
}
|
|
@ -214,13 +214,3 @@ func (i *idTokenVerifier) ACR() oidc.ACRVerifier {
|
||||||
func (i *idTokenVerifier) MaxAge() time.Duration {
|
func (i *idTokenVerifier) MaxAge() time.Duration {
|
||||||
return i.maxAge
|
return i.maxAge
|
||||||
}
|
}
|
||||||
|
|
||||||
//deprecated: Use IDTokenVerifier (or oidc.Verifier)
|
|
||||||
type Verifier interface {
|
|
||||||
|
|
||||||
//Verify checks the access_token and id_token and returns the `id token claims`
|
|
||||||
Verify(ctx context.Context, accessToken, idTokenString string) (*oidc.IDTokenClaims, error)
|
|
||||||
|
|
||||||
//VerifyIDToken checks the id_token only and returns its `id token claims`
|
|
||||||
VerifyIDToken(ctx context.Context, idTokenString string) (*oidc.IDTokenClaims, error)
|
|
||||||
}
|
|
123
pkg/client/rs/resource_server.go
Normal file
123
pkg/client/rs/resource_server.go
Normal file
|
@ -0,0 +1,123 @@
|
||||||
|
package rs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/caos/oidc/pkg/client"
|
||||||
|
"github.com/caos/oidc/pkg/oidc"
|
||||||
|
"github.com/caos/oidc/pkg/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ResourceServer interface {
|
||||||
|
IntrospectionURL() string
|
||||||
|
HttpClient() *http.Client
|
||||||
|
AuthFn() (interface{}, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type resourceServer struct {
|
||||||
|
issuer string
|
||||||
|
tokenURL string
|
||||||
|
introspectURL string
|
||||||
|
httpClient *http.Client
|
||||||
|
authFn func() (interface{}, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *resourceServer) IntrospectionURL() string {
|
||||||
|
return r.introspectURL
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *resourceServer) HttpClient() *http.Client {
|
||||||
|
return r.httpClient
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *resourceServer) AuthFn() (interface{}, error) {
|
||||||
|
return r.authFn()
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewResourceServerClientCredentials(issuer, clientID, clientSecret string, option Option) (ResourceServer, error) {
|
||||||
|
authorizer := func() (interface{}, error) {
|
||||||
|
return utils.AuthorizeBasic(clientID, clientSecret), nil
|
||||||
|
}
|
||||||
|
return newResourceServer(issuer, authorizer, option)
|
||||||
|
}
|
||||||
|
func NewResourceServerJWTProfile(issuer, clientID, keyID string, key []byte, options ...Option) (ResourceServer, error) {
|
||||||
|
signer, err := client.NewSignerFromPrivateKeyByte(key, keyID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
authorizer := func() (interface{}, error) {
|
||||||
|
assertion, err := client.SignedJWTProfileAssertion(clientID, []string{issuer}, time.Hour, signer)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return client.ClientAssertionFormAuthorization(assertion), nil
|
||||||
|
}
|
||||||
|
return newResourceServer(issuer, authorizer, options...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newResourceServer(issuer string, authorizer func() (interface{}, error), options ...Option) (*resourceServer, error) {
|
||||||
|
rs := &resourceServer{
|
||||||
|
issuer: issuer,
|
||||||
|
httpClient: utils.DefaultHTTPClient,
|
||||||
|
}
|
||||||
|
for _, optFunc := range options {
|
||||||
|
optFunc(rs)
|
||||||
|
}
|
||||||
|
if rs.introspectURL == "" || rs.tokenURL == "" {
|
||||||
|
config, err := client.Discover(rs.issuer, rs.httpClient)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
rs.tokenURL = config.TokenEndpoint
|
||||||
|
rs.introspectURL = config.IntrospectionEndpoint
|
||||||
|
}
|
||||||
|
if rs.introspectURL == "" || rs.tokenURL == "" {
|
||||||
|
return nil, errors.New("introspectURL and/or tokenURL is empty: please provide with either `WithStaticEndpoints` or a discovery url")
|
||||||
|
}
|
||||||
|
rs.authFn = authorizer
|
||||||
|
return rs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewResourceServerFromKeyFile(issuer, path string, options ...Option) (ResourceServer, error) {
|
||||||
|
c, err := client.ConfigFromKeyFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return NewResourceServerJWTProfile(issuer, c.ClientID, c.KeyID, []byte(c.Key), options...)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Option func(*resourceServer)
|
||||||
|
|
||||||
|
//WithClient provides the ability to set an http client to be used for the resource server
|
||||||
|
func WithClient(client *http.Client) Option {
|
||||||
|
return func(server *resourceServer) {
|
||||||
|
server.httpClient = client
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//WithStaticEndpoints provides the ability to set static token and introspect URL
|
||||||
|
func WithStaticEndpoints(tokenURL, introspectURL string) Option {
|
||||||
|
return func(server *resourceServer) {
|
||||||
|
server.tokenURL = tokenURL
|
||||||
|
server.introspectURL = introspectURL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Introspect(ctx context.Context, rp ResourceServer, token string) (oidc.IntrospectionResponse, error) {
|
||||||
|
authFn, err := rp.AuthFn()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req, err := utils.FormRequest(rp.IntrospectionURL(), &oidc.IntrospectionRequest{Token: token}, client.Encoder, authFn)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
resp := oidc.NewIntrospectionResponse()
|
||||||
|
if err := utils.HttpRequest(rp.HttpClient(), req, resp); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return resp, nil
|
||||||
|
}
|
|
@ -24,7 +24,7 @@ func NewSHACodeChallenge(code string) string {
|
||||||
|
|
||||||
func VerifyCodeChallenge(c *CodeChallenge, codeVerifier string) bool {
|
func VerifyCodeChallenge(c *CodeChallenge, codeVerifier string) bool {
|
||||||
if c == nil {
|
if c == nil {
|
||||||
return false //TODO: ?
|
return false
|
||||||
}
|
}
|
||||||
if c.Method == CodeChallengeMethodS256 {
|
if c.Method == CodeChallengeMethodS256 {
|
||||||
codeVerifier = NewSHACodeChallenge(codeVerifier)
|
codeVerifier = NewSHACodeChallenge(codeVerifier)
|
||||||
|
|
|
@ -1,25 +1,68 @@
|
||||||
package oidc
|
package oidc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"golang.org/x/text/language"
|
||||||
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
DiscoveryEndpoint = "/.well-known/openid-configuration"
|
DiscoveryEndpoint = "/.well-known/openid-configuration"
|
||||||
)
|
)
|
||||||
|
|
||||||
type DiscoveryConfiguration struct {
|
type DiscoveryConfiguration struct {
|
||||||
Issuer string `json:"issuer,omitempty"`
|
Issuer string `json:"issuer,omitempty"`
|
||||||
AuthorizationEndpoint string `json:"authorization_endpoint,omitempty"`
|
AuthorizationEndpoint string `json:"authorization_endpoint,omitempty"`
|
||||||
TokenEndpoint string `json:"token_endpoint,omitempty"`
|
TokenEndpoint string `json:"token_endpoint,omitempty"`
|
||||||
IntrospectionEndpoint string `json:"introspection_endpoint,omitempty"`
|
IntrospectionEndpoint string `json:"introspection_endpoint,omitempty"`
|
||||||
UserinfoEndpoint string `json:"userinfo_endpoint,omitempty"`
|
UserinfoEndpoint string `json:"userinfo_endpoint,omitempty"`
|
||||||
EndSessionEndpoint string `json:"end_session_endpoint,omitempty"`
|
RevocationEndpoint string `json:"revocation_endpoint,omitempty"`
|
||||||
CheckSessionIframe string `json:"check_session_iframe,omitempty"`
|
EndSessionEndpoint string `json:"end_session_endpoint,omitempty"`
|
||||||
JwksURI string `json:"jwks_uri,omitempty"`
|
CheckSessionIframe string `json:"check_session_iframe,omitempty"`
|
||||||
ScopesSupported []string `json:"scopes_supported,omitempty"`
|
JwksURI string `json:"jwks_uri,omitempty"`
|
||||||
ResponseTypesSupported []string `json:"response_types_supported,omitempty"`
|
ScopesSupported []string `json:"scopes_supported,omitempty"`
|
||||||
ResponseModesSupported []string `json:"response_modes_supported,omitempty"`
|
ResponseTypesSupported []string `json:"response_types_supported,omitempty"`
|
||||||
GrantTypesSupported []string `json:"grant_types_supported,omitempty"`
|
ResponseModesSupported []string `json:"response_modes_supported,omitempty"`
|
||||||
SubjectTypesSupported []string `json:"subject_types_supported,omitempty"`
|
GrantTypesSupported []GrantType `json:"grant_types_supported,omitempty"`
|
||||||
IDTokenSigningAlgValuesSupported []string `json:"id_token_signing_alg_values_supported,omitempty"`
|
ACRValuesSupported []string `json:"acr_values_supported,omitempty"`
|
||||||
TokenEndpointAuthMethodsSupported []string `json:"token_endpoint_auth_methods_supported,omitempty"`
|
SubjectTypesSupported []string `json:"subject_types_supported,omitempty"`
|
||||||
CodeChallengeMethodsSupported []string `json:"code_challenge_methods_supported,omitempty"`
|
IDTokenSigningAlgValuesSupported []string `json:"id_token_signing_alg_values_supported,omitempty"`
|
||||||
ClaimsSupported []string `json:"claims_supported,omitempty"`
|
IDTokenEncryptionAlgValuesSupported []string `json:"id_token_encryption_alg_values_supported,omitempty"`
|
||||||
|
IDTokenEncryptionEncValuesSupported []string `json:"id_token_encryption_enc_values_supported,omitempty"`
|
||||||
|
UserinfoSigningAlgValuesSupported []string `json:"userinfo_signing_alg_values_supported,omitempty"`
|
||||||
|
UserinfoEncryptionAlgValuesSupported []string `json:"userinfo_encryption_alg_values_supported,omitempty"`
|
||||||
|
UserinfoEncryptionEncValuesSupported []string `json:"userinfo_encryption_enc_values_supported,omitempty"`
|
||||||
|
RequestObjectSigningAlgValuesSupported []string `json:"request_object_signing_alg_values_supported,omitempty"`
|
||||||
|
RequestObjectEncryptionAlgValuesSupported []string `json:"request_object_encryption_alg_values_supported,omitempty"`
|
||||||
|
RequestObjectEncryptionEncValuesSupported []string `json:"request_object_encryption_enc_values_supported,omitempty"`
|
||||||
|
TokenEndpointAuthMethodsSupported []AuthMethod `json:"token_endpoint_auth_methods_supported,omitempty"`
|
||||||
|
TokenEndpointAuthSigningAlgValuesSupported []string `json:"token_endpoint_auth_signing_alg_values_supported,omitempty"`
|
||||||
|
RevocationEndpointAuthMethodsSupported []AuthMethod `json:"revocation_endpoint_auth_methods_supported,omitempty"`
|
||||||
|
RevocationEndpointAuthSigningAlgValuesSupported []string `json:"revocation_endpoint_auth_signing_alg_values_supported,omitempty"`
|
||||||
|
IntrospectionEndpointAuthMethodsSupported []AuthMethod `json:"introspection_endpoint_auth_methods_supported,omitempty"`
|
||||||
|
IntrospectionEndpointAuthSigningAlgValuesSupported []string `json:"introspection_endpoint_auth_signing_alg_values_supported,omitempty"`
|
||||||
|
DisplayValuesSupported []Display `json:"display_values_supported,omitempty"`
|
||||||
|
ClaimTypesSupported []string `json:"claim_types_supported,omitempty"`
|
||||||
|
ClaimsSupported []string `json:"claims_supported,omitempty"`
|
||||||
|
ClaimsParameterSupported bool `json:"claims_parameter_supported,omitempty"`
|
||||||
|
CodeChallengeMethodsSupported []CodeChallengeMethod `json:"code_challenge_methods_supported,omitempty"`
|
||||||
|
ServiceDocumentation string `json:"service_documentation,omitempty"`
|
||||||
|
ClaimsLocalesSupported []language.Tag `json:"claims_locales_supported,omitempty"`
|
||||||
|
UILocalesSupported []language.Tag `json:"ui_locales_supported,omitempty"`
|
||||||
|
RequestParameterSupported bool `json:"request_parameter_supported,omitempty"`
|
||||||
|
RequestURIParameterSupported bool `json:"request_uri_parameter_supported"` //no omitempty because: If omitted, the default value is true
|
||||||
|
RequireRequestURIRegistration bool `json:"require_request_uri_registration,omitempty"`
|
||||||
|
OPPolicyURI string `json:"op_policy_uri,omitempty"`
|
||||||
|
OPTermsOfServiceURI string `json:"op_tos_uri,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AuthMethod string
|
||||||
|
|
||||||
|
const (
|
||||||
|
AuthMethodBasic AuthMethod = "client_secret_basic"
|
||||||
|
AuthMethodPost AuthMethod = "client_secret_post"
|
||||||
|
AuthMethodNone AuthMethod = "none"
|
||||||
|
AuthMethodPrivateKeyJWT AuthMethod = "private_key_jwt"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
GrantTypeImplicit GrantType = "implicit"
|
||||||
|
)
|
||||||
|
|
|
@ -1,9 +1,5 @@
|
||||||
package tokenexchange
|
package tokenexchange
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/caos/oidc/pkg/oidc"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
AccessTokenType = "urn:ietf:params:oauth:token-type:access_token"
|
AccessTokenType = "urn:ietf:params:oauth:token-type:access_token"
|
||||||
RefreshTokenType = "urn:ietf:params:oauth:token-type:refresh_token"
|
RefreshTokenType = "urn:ietf:params:oauth:token-type:refresh_token"
|
||||||
|
@ -26,22 +22,6 @@ type TokenExchangeRequest struct {
|
||||||
requestedTokenType string `schema:"requested_token_type"`
|
requestedTokenType string `schema:"requested_token_type"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type JWTProfileRequest struct {
|
|
||||||
Assertion string `schema:"assertion"`
|
|
||||||
Scope oidc.Scopes `schema:"scope"`
|
|
||||||
GrantType oidc.GrantType `schema:"grant_type"`
|
|
||||||
}
|
|
||||||
|
|
||||||
//ClientCredentialsGrantBasic creates an oauth2 `Client Credentials` Grant
|
|
||||||
//sneding client_id and client_secret as basic auth header
|
|
||||||
func NewJWTProfileRequest(assertion string, scopes ...string) *JWTProfileRequest {
|
|
||||||
return &JWTProfileRequest{
|
|
||||||
GrantType: oidc.GrantTypeBearer,
|
|
||||||
Assertion: assertion,
|
|
||||||
Scope: scopes,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewTokenExchangeRequest(subjectToken, subjectTokenType string, opts ...TokenExchangeOption) *TokenExchangeRequest {
|
func NewTokenExchangeRequest(subjectToken, subjectTokenType string, opts ...TokenExchangeOption) *TokenExchangeRequest {
|
||||||
t := &TokenExchangeRequest{
|
t := &TokenExchangeRequest{
|
||||||
grantType: TokenExchangeGrantType,
|
grantType: TokenExchangeGrantType,
|
||||||
|
|
276
pkg/oidc/introspection.go
Normal file
276
pkg/oidc/introspection.go
Normal file
|
@ -0,0 +1,276 @@
|
||||||
|
package oidc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/text/language"
|
||||||
|
)
|
||||||
|
|
||||||
|
type IntrospectionRequest struct {
|
||||||
|
Token string `schema:"token"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClientAssertionParams struct {
|
||||||
|
ClientAssertion string `schema:"client_assertion"`
|
||||||
|
ClientAssertionType string `schema:"client_assertion_type"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type IntrospectionResponse interface {
|
||||||
|
UserInfoSetter
|
||||||
|
SetActive(bool)
|
||||||
|
IsActive() bool
|
||||||
|
SetScopes(scopes Scopes)
|
||||||
|
SetClientID(id string)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewIntrospectionResponse() IntrospectionResponse {
|
||||||
|
return &introspectionResponse{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type introspectionResponse struct {
|
||||||
|
Active bool `json:"active"`
|
||||||
|
Scope Scopes `json:"scope,omitempty"`
|
||||||
|
ClientID string `json:"client_id,omitempty"`
|
||||||
|
Subject string `json:"sub,omitempty"`
|
||||||
|
userInfoProfile
|
||||||
|
userInfoEmail
|
||||||
|
userInfoPhone
|
||||||
|
|
||||||
|
Address UserInfoAddress `json:"address,omitempty"`
|
||||||
|
claims map[string]interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *introspectionResponse) IsActive() bool {
|
||||||
|
return u.Active
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *introspectionResponse) SetScopes(scope Scopes) {
|
||||||
|
u.Scope = scope
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *introspectionResponse) SetClientID(id string) {
|
||||||
|
u.ClientID = id
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *introspectionResponse) GetSubject() string {
|
||||||
|
return u.Subject
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *introspectionResponse) GetName() string {
|
||||||
|
return u.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *introspectionResponse) GetGivenName() string {
|
||||||
|
return u.GivenName
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *introspectionResponse) GetFamilyName() string {
|
||||||
|
return u.FamilyName
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *introspectionResponse) GetMiddleName() string {
|
||||||
|
return u.MiddleName
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *introspectionResponse) GetNickname() string {
|
||||||
|
return u.Nickname
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *introspectionResponse) GetProfile() string {
|
||||||
|
return u.Profile
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *introspectionResponse) GetPicture() string {
|
||||||
|
return u.Picture
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *introspectionResponse) GetWebsite() string {
|
||||||
|
return u.Website
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *introspectionResponse) GetGender() Gender {
|
||||||
|
return u.Gender
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *introspectionResponse) GetBirthdate() string {
|
||||||
|
return u.Birthdate
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *introspectionResponse) GetZoneinfo() string {
|
||||||
|
return u.Zoneinfo
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *introspectionResponse) GetLocale() language.Tag {
|
||||||
|
return u.Locale
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *introspectionResponse) GetPreferredUsername() string {
|
||||||
|
return u.PreferredUsername
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *introspectionResponse) GetEmail() string {
|
||||||
|
return u.Email
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *introspectionResponse) IsEmailVerified() bool {
|
||||||
|
return u.EmailVerified
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *introspectionResponse) GetPhoneNumber() string {
|
||||||
|
return u.PhoneNumber
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *introspectionResponse) IsPhoneNumberVerified() bool {
|
||||||
|
return u.PhoneNumberVerified
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *introspectionResponse) GetAddress() UserInfoAddress {
|
||||||
|
return u.Address
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *introspectionResponse) GetClaim(key string) interface{} {
|
||||||
|
return u.claims[key]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *introspectionResponse) SetActive(active bool) {
|
||||||
|
u.Active = active
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *introspectionResponse) SetSubject(sub string) {
|
||||||
|
u.Subject = sub
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *introspectionResponse) SetName(name string) {
|
||||||
|
u.Name = name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *introspectionResponse) SetGivenName(name string) {
|
||||||
|
u.GivenName = name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *introspectionResponse) SetFamilyName(name string) {
|
||||||
|
u.FamilyName = name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *introspectionResponse) SetMiddleName(name string) {
|
||||||
|
u.MiddleName = name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *introspectionResponse) SetNickname(name string) {
|
||||||
|
u.Nickname = name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *introspectionResponse) SetUpdatedAt(date time.Time) {
|
||||||
|
u.UpdatedAt = Time(date)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *introspectionResponse) SetProfile(profile string) {
|
||||||
|
u.Profile = profile
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *introspectionResponse) SetPicture(picture string) {
|
||||||
|
u.Picture = picture
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *introspectionResponse) SetWebsite(website string) {
|
||||||
|
u.Website = website
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *introspectionResponse) SetGender(gender Gender) {
|
||||||
|
u.Gender = gender
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *introspectionResponse) SetBirthdate(birthdate string) {
|
||||||
|
u.Birthdate = birthdate
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *introspectionResponse) SetZoneinfo(zoneInfo string) {
|
||||||
|
u.Zoneinfo = zoneInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *introspectionResponse) SetLocale(locale language.Tag) {
|
||||||
|
u.Locale = locale
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *introspectionResponse) SetPreferredUsername(name string) {
|
||||||
|
u.PreferredUsername = name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *introspectionResponse) SetEmail(email string, verified bool) {
|
||||||
|
u.Email = email
|
||||||
|
u.EmailVerified = verified
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *introspectionResponse) SetPhone(phone string, verified bool) {
|
||||||
|
u.PhoneNumber = phone
|
||||||
|
u.PhoneNumberVerified = verified
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *introspectionResponse) SetAddress(address UserInfoAddress) {
|
||||||
|
u.Address = address
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *introspectionResponse) AppendClaims(key string, value interface{}) {
|
||||||
|
if u.claims == nil {
|
||||||
|
u.claims = make(map[string]interface{})
|
||||||
|
}
|
||||||
|
u.claims[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *introspectionResponse) MarshalJSON() ([]byte, error) {
|
||||||
|
type Alias introspectionResponse
|
||||||
|
a := &struct {
|
||||||
|
*Alias
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
18
pkg/oidc/jwt_profile.go
Normal file
18
pkg/oidc/jwt_profile.go
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
package oidc
|
||||||
|
|
||||||
|
type JWTProfileGrantRequest struct {
|
||||||
|
Assertion string `schema:"assertion"`
|
||||||
|
Scope Scopes `schema:"scope"`
|
||||||
|
GrantType GrantType `schema:"grant_type"`
|
||||||
|
}
|
||||||
|
|
||||||
|
//NewJWTProfileGrantRequest creates an oauth2 `JSON Web Token (JWT) Profile` Grant
|
||||||
|
//`urn:ietf:params:oauth:grant-type:jwt-bearer`
|
||||||
|
//sending a self-signed jwt as assertion
|
||||||
|
func NewJWTProfileGrantRequest(assertion string, scopes ...string) *JWTProfileGrantRequest {
|
||||||
|
return &JWTProfileGrantRequest{
|
||||||
|
GrantType: GrantTypeBearer,
|
||||||
|
Assertion: assertion,
|
||||||
|
Scope: scopes,
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,10 @@
|
||||||
package oidc
|
package oidc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/x509"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"encoding/pem"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -14,6 +17,8 @@ import (
|
||||||
const (
|
const (
|
||||||
//BearerToken defines the token_type `Bearer`, which is returned in a successful token response
|
//BearerToken defines the token_type `Bearer`, which is returned in a successful token response
|
||||||
BearerToken = "Bearer"
|
BearerToken = "Bearer"
|
||||||
|
|
||||||
|
PrefixBearer = BearerToken + " "
|
||||||
)
|
)
|
||||||
|
|
||||||
type Tokens struct {
|
type Tokens struct {
|
||||||
|
@ -397,7 +402,7 @@ type AccessTokenResponse struct {
|
||||||
type JWTProfileAssertion struct {
|
type JWTProfileAssertion struct {
|
||||||
PrivateKeyID string `json:"-"`
|
PrivateKeyID string `json:"-"`
|
||||||
PrivateKey []byte `json:"-"`
|
PrivateKey []byte `json:"-"`
|
||||||
Issuer string `json:"issuer"`
|
Issuer string `json:"iss"`
|
||||||
Subject string `json:"sub"`
|
Subject string `json:"sub"`
|
||||||
Audience Audience `json:"aud"`
|
Audience Audience `json:"aud"`
|
||||||
Expiration Time `json:"exp"`
|
Expiration Time `json:"exp"`
|
||||||
|
@ -412,6 +417,19 @@ func NewJWTProfileAssertionFromKeyJSON(filename string, audience []string) (*JWT
|
||||||
return NewJWTProfileAssertionFromFileData(data, audience)
|
return NewJWTProfileAssertionFromFileData(data, audience)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NewJWTProfileAssertionStringFromFileData(data []byte, audience []string) (string, error) {
|
||||||
|
keyData := new(struct {
|
||||||
|
KeyID string `json:"keyId"`
|
||||||
|
Key string `json:"key"`
|
||||||
|
UserID string `json:"userId"`
|
||||||
|
})
|
||||||
|
err := json.Unmarshal(data, keyData)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return GenerateJWTProfileToken(NewJWTProfileAssertion(keyData.UserID, keyData.KeyID, audience, []byte(keyData.Key)))
|
||||||
|
}
|
||||||
|
|
||||||
func NewJWTProfileAssertionFromFileData(data []byte, audience []string) (*JWTProfileAssertion, error) {
|
func NewJWTProfileAssertionFromFileData(data []byte, audience []string) (*JWTProfileAssertion, error) {
|
||||||
keyData := new(struct {
|
keyData := new(struct {
|
||||||
KeyID string `json:"keyId"`
|
KeyID string `json:"keyId"`
|
||||||
|
@ -454,3 +472,46 @@ func AppendClientIDToAudience(clientID string, audience []string) []string {
|
||||||
}
|
}
|
||||||
return append(audience, clientID)
|
return append(audience, clientID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GenerateJWTProfileToken(assertion *JWTProfileAssertion) (string, error) {
|
||||||
|
privateKey, err := bytesToPrivateKey(assertion.PrivateKey)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
key := jose.SigningKey{
|
||||||
|
Algorithm: jose.RS256,
|
||||||
|
Key: &jose.JSONWebKey{Key: privateKey, KeyID: assertion.PrivateKeyID},
|
||||||
|
}
|
||||||
|
signer, err := jose.NewSigner(key, &jose.SignerOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
marshalledAssertion, err := json.Marshal(assertion)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
signedAssertion, err := signer.Sign(marshalledAssertion)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return signedAssertion.CompactSerialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
func bytesToPrivateKey(priv []byte) (*rsa.PrivateKey, error) {
|
||||||
|
block, _ := pem.Decode(priv)
|
||||||
|
enc := x509.IsEncryptedPEMBlock(block)
|
||||||
|
b := block.Bytes
|
||||||
|
var err error
|
||||||
|
if enc {
|
||||||
|
b, err = x509.DecryptPEMBlock(block, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
key, err := x509.ParsePKCS1PrivateKey(b)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return key, nil
|
||||||
|
}
|
||||||
|
|
|
@ -15,6 +15,10 @@ const (
|
||||||
|
|
||||||
//GrantTypeTokenExchange defines the grant_type `urn:ietf:params:oauth:grant-type:token-exchange` used for the OAuth Token Exchange Grant
|
//GrantTypeTokenExchange defines the grant_type `urn:ietf:params:oauth:grant-type:token-exchange` used for the OAuth Token Exchange Grant
|
||||||
GrantTypeTokenExchange GrantType = "urn:ietf:params:oauth:grant-type:token-exchange"
|
GrantTypeTokenExchange GrantType = "urn:ietf:params:oauth:grant-type:token-exchange"
|
||||||
|
|
||||||
|
//ClientAssertionTypeJWTAssertion defines the client_assertion_type `urn:ietf:params:oauth:client-assertion-type:jwt-bearer`
|
||||||
|
//used for the OAuth JWT Profile Client Authentication
|
||||||
|
ClientAssertionTypeJWTAssertion = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
|
||||||
)
|
)
|
||||||
|
|
||||||
type GrantType string
|
type GrantType string
|
||||||
|
@ -27,11 +31,13 @@ type TokenRequest interface {
|
||||||
type TokenRequestType GrantType
|
type TokenRequestType GrantType
|
||||||
|
|
||||||
type AccessTokenRequest struct {
|
type AccessTokenRequest struct {
|
||||||
Code string `schema:"code"`
|
Code string `schema:"code"`
|
||||||
RedirectURI string `schema:"redirect_uri"`
|
RedirectURI string `schema:"redirect_uri"`
|
||||||
ClientID string `schema:"client_id"`
|
ClientID string `schema:"client_id"`
|
||||||
ClientSecret string `schema:"client_secret"`
|
ClientSecret string `schema:"client_secret"`
|
||||||
CodeVerifier string `schema:"code_verifier"`
|
CodeVerifier string `schema:"code_verifier"`
|
||||||
|
ClientAssertion string `schema:"client_assertion"`
|
||||||
|
ClientAssertionType string `schema:"client_assertion_type"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *AccessTokenRequest) GrantType() GrantType {
|
func (a *AccessTokenRequest) GrantType() GrantType {
|
||||||
|
|
|
@ -73,6 +73,19 @@ func (s *Scopes) MarshalText() ([]byte, error) {
|
||||||
return []byte(s.Encode()), nil
|
return []byte(s.Encode()), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Scopes) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal((*s).Encode())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Scopes) UnmarshalJSON(data []byte) error {
|
||||||
|
var str string
|
||||||
|
if err := json.Unmarshal(data, &str); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*s = strings.Split(str, " ")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type Time time.Time
|
type Time time.Time
|
||||||
|
|
||||||
func (t *Time) UnmarshalJSON(data []byte) error {
|
func (t *Time) UnmarshalJSON(data []byte) error {
|
||||||
|
|
|
@ -6,8 +6,6 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"golang.org/x/text/language"
|
"golang.org/x/text/language"
|
||||||
|
|
||||||
"github.com/caos/oidc/pkg/utils"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type UserInfo interface {
|
type UserInfo interface {
|
||||||
|
@ -351,11 +349,12 @@ func (i *userinfo) MarshalJSON() ([]byte, error) {
|
||||||
return b, nil
|
return b, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
claims, err := json.Marshal(i.claims)
|
err = json.Unmarshal(b, &i.claims)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("jws: invalid map of custom claims %v", i.claims)
|
return nil, fmt.Errorf("jws: invalid map of custom claims %v", i.claims)
|
||||||
}
|
}
|
||||||
return utils.ConcatenateJSON(b, claims)
|
|
||||||
|
return json.Marshal(i.claims)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *userinfo) UnmarshalJSON(data []byte) error {
|
func (i *userinfo) UnmarshalJSON(data []byte) error {
|
||||||
|
@ -372,6 +371,10 @@ func (i *userinfo) UnmarshalJSON(data []byte) error {
|
||||||
|
|
||||||
i.UpdatedAt = Time(time.Unix(a.UpdatedAt, 0).UTC())
|
i.UpdatedAt = Time(time.Unix(a.UpdatedAt, 0).UTC())
|
||||||
|
|
||||||
|
if err := json.Unmarshal(data, &i.claims); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
|
|
||||||
|
@ -12,6 +13,23 @@ import (
|
||||||
"github.com/caos/oidc/pkg/utils"
|
"github.com/caos/oidc/pkg/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type AuthRequest interface {
|
||||||
|
GetID() string
|
||||||
|
GetACR() string
|
||||||
|
GetAMR() []string
|
||||||
|
GetAudience() []string
|
||||||
|
GetAuthTime() time.Time
|
||||||
|
GetClientID() string
|
||||||
|
GetCodeChallenge() *oidc.CodeChallenge
|
||||||
|
GetNonce() string
|
||||||
|
GetRedirectURI() string
|
||||||
|
GetResponseType() oidc.ResponseType
|
||||||
|
GetScopes() []string
|
||||||
|
GetState() string
|
||||||
|
GetSubject() string
|
||||||
|
Done() bool
|
||||||
|
}
|
||||||
|
|
||||||
type Authorizer interface {
|
type Authorizer interface {
|
||||||
Storage() Storage
|
Storage() Storage
|
||||||
Decoder() utils.Decoder
|
Decoder() utils.Decoder
|
||||||
|
|
|
@ -28,7 +28,7 @@ type Client interface {
|
||||||
RedirectURIs() []string
|
RedirectURIs() []string
|
||||||
PostLogoutRedirectURIs() []string
|
PostLogoutRedirectURIs() []string
|
||||||
ApplicationType() ApplicationType
|
ApplicationType() ApplicationType
|
||||||
AuthMethod() AuthMethod
|
AuthMethod() oidc.AuthMethod
|
||||||
ResponseTypes() []oidc.ResponseType
|
ResponseTypes() []oidc.ResponseType
|
||||||
LoginURL(string) string
|
LoginURL(string) string
|
||||||
AccessTokenType() AccessTokenType
|
AccessTokenType() AccessTokenType
|
||||||
|
|
|
@ -13,12 +13,14 @@ type Configuration interface {
|
||||||
Issuer() string
|
Issuer() string
|
||||||
AuthorizationEndpoint() Endpoint
|
AuthorizationEndpoint() Endpoint
|
||||||
TokenEndpoint() Endpoint
|
TokenEndpoint() Endpoint
|
||||||
|
IntrospectionEndpoint() Endpoint
|
||||||
UserinfoEndpoint() Endpoint
|
UserinfoEndpoint() Endpoint
|
||||||
EndSessionEndpoint() Endpoint
|
EndSessionEndpoint() Endpoint
|
||||||
KeysEndpoint() Endpoint
|
KeysEndpoint() Endpoint
|
||||||
|
|
||||||
AuthMethodPostSupported() bool
|
AuthMethodPostSupported() bool
|
||||||
CodeMethodS256Supported() bool
|
CodeMethodS256Supported() bool
|
||||||
|
AuthMethodPrivateKeyJWTSupported() bool
|
||||||
GrantTypeTokenExchangeSupported() bool
|
GrantTypeTokenExchangeSupported() bool
|
||||||
GrantTypeJWTAuthorizationSupported() bool
|
GrantTypeJWTAuthorizationSupported() bool
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,8 @@ package op
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"golang.org/x/text/language"
|
||||||
|
|
||||||
"github.com/caos/oidc/pkg/oidc"
|
"github.com/caos/oidc/pkg/oidc"
|
||||||
"github.com/caos/oidc/pkg/utils"
|
"github.com/caos/oidc/pkg/utils"
|
||||||
)
|
)
|
||||||
|
@ -19,22 +21,23 @@ func Discover(w http.ResponseWriter, config *oidc.DiscoveryConfiguration) {
|
||||||
|
|
||||||
func CreateDiscoveryConfig(c Configuration, s Signer) *oidc.DiscoveryConfiguration {
|
func CreateDiscoveryConfig(c Configuration, s Signer) *oidc.DiscoveryConfiguration {
|
||||||
return &oidc.DiscoveryConfiguration{
|
return &oidc.DiscoveryConfiguration{
|
||||||
Issuer: c.Issuer(),
|
Issuer: c.Issuer(),
|
||||||
AuthorizationEndpoint: c.AuthorizationEndpoint().Absolute(c.Issuer()),
|
AuthorizationEndpoint: c.AuthorizationEndpoint().Absolute(c.Issuer()),
|
||||||
TokenEndpoint: c.TokenEndpoint().Absolute(c.Issuer()),
|
TokenEndpoint: c.TokenEndpoint().Absolute(c.Issuer()),
|
||||||
// IntrospectionEndpoint: c.Intro().Absolute(c.Issuer()),
|
IntrospectionEndpoint: c.IntrospectionEndpoint().Absolute(c.Issuer()),
|
||||||
UserinfoEndpoint: c.UserinfoEndpoint().Absolute(c.Issuer()),
|
UserinfoEndpoint: c.UserinfoEndpoint().Absolute(c.Issuer()),
|
||||||
EndSessionEndpoint: c.EndSessionEndpoint().Absolute(c.Issuer()),
|
EndSessionEndpoint: c.EndSessionEndpoint().Absolute(c.Issuer()),
|
||||||
// CheckSessionIframe: c.TokenEndpoint().Absolute(c.Issuer())(c.CheckSessionIframe),
|
JwksURI: c.KeysEndpoint().Absolute(c.Issuer()),
|
||||||
JwksURI: c.KeysEndpoint().Absolute(c.Issuer()),
|
ScopesSupported: Scopes(c),
|
||||||
ScopesSupported: Scopes(c),
|
ResponseTypesSupported: ResponseTypes(c),
|
||||||
ResponseTypesSupported: ResponseTypes(c),
|
GrantTypesSupported: GrantTypes(c),
|
||||||
GrantTypesSupported: GrantTypes(c),
|
SubjectTypesSupported: SubjectTypes(c),
|
||||||
ClaimsSupported: SupportedClaims(c),
|
IDTokenSigningAlgValuesSupported: SigAlgorithms(s),
|
||||||
IDTokenSigningAlgValuesSupported: SigAlgorithms(s),
|
TokenEndpointAuthMethodsSupported: AuthMethodsTokenEndpoint(c),
|
||||||
SubjectTypesSupported: SubjectTypes(c),
|
IntrospectionEndpointAuthMethodsSupported: AuthMethodsIntrospectionEndpoint(c),
|
||||||
TokenEndpointAuthMethodsSupported: AuthMethods(c),
|
ClaimsSupported: SupportedClaims(c),
|
||||||
CodeChallengeMethodsSupported: CodeChallengeMethods(c),
|
CodeChallengeMethodsSupported: CodeChallengeMethods(c),
|
||||||
|
UILocalesSupported: UILocales(c),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -58,15 +61,16 @@ func ResponseTypes(c Configuration) []string {
|
||||||
} //TODO: ok for now, check later if dynamic needed
|
} //TODO: ok for now, check later if dynamic needed
|
||||||
}
|
}
|
||||||
|
|
||||||
func GrantTypes(c Configuration) []string {
|
func GrantTypes(c Configuration) []oidc.GrantType {
|
||||||
grantTypes := []string{
|
grantTypes := []oidc.GrantType{
|
||||||
string(oidc.GrantTypeCode),
|
oidc.GrantTypeCode,
|
||||||
|
oidc.GrantTypeImplicit,
|
||||||
}
|
}
|
||||||
if c.GrantTypeTokenExchangeSupported() {
|
if c.GrantTypeTokenExchangeSupported() {
|
||||||
grantTypes = append(grantTypes, string(oidc.GrantTypeTokenExchange))
|
grantTypes = append(grantTypes, oidc.GrantTypeTokenExchange)
|
||||||
}
|
}
|
||||||
if c.GrantTypeJWTAuthorizationSupported() {
|
if c.GrantTypeJWTAuthorizationSupported() {
|
||||||
grantTypes = append(grantTypes, string(oidc.GrantTypeBearer))
|
grantTypes = append(grantTypes, oidc.GrantTypeBearer)
|
||||||
}
|
}
|
||||||
return grantTypes
|
return grantTypes
|
||||||
}
|
}
|
||||||
|
@ -108,20 +112,41 @@ func SubjectTypes(c Configuration) []string {
|
||||||
return []string{"public"} //TODO: config
|
return []string{"public"} //TODO: config
|
||||||
}
|
}
|
||||||
|
|
||||||
func AuthMethods(c Configuration) []string {
|
func AuthMethodsTokenEndpoint(c Configuration) []oidc.AuthMethod {
|
||||||
authMethods := []string{
|
authMethods := []oidc.AuthMethod{
|
||||||
string(AuthMethodBasic),
|
oidc.AuthMethodNone,
|
||||||
|
oidc.AuthMethodBasic,
|
||||||
}
|
}
|
||||||
if c.AuthMethodPostSupported() {
|
if c.AuthMethodPostSupported() {
|
||||||
authMethods = append(authMethods, string(AuthMethodPost))
|
authMethods = append(authMethods, oidc.AuthMethodPost)
|
||||||
|
}
|
||||||
|
if c.AuthMethodPrivateKeyJWTSupported() {
|
||||||
|
authMethods = append(authMethods, oidc.AuthMethodPrivateKeyJWT)
|
||||||
}
|
}
|
||||||
return authMethods
|
return authMethods
|
||||||
}
|
}
|
||||||
|
|
||||||
func CodeChallengeMethods(c Configuration) []string {
|
func AuthMethodsIntrospectionEndpoint(c Configuration) []oidc.AuthMethod {
|
||||||
codeMethods := make([]string, 0, 1)
|
authMethods := []oidc.AuthMethod{
|
||||||
|
oidc.AuthMethodBasic,
|
||||||
|
}
|
||||||
|
if c.AuthMethodPrivateKeyJWTSupported() {
|
||||||
|
authMethods = append(authMethods, oidc.AuthMethodPrivateKeyJWT)
|
||||||
|
}
|
||||||
|
return authMethods
|
||||||
|
}
|
||||||
|
|
||||||
|
func CodeChallengeMethods(c Configuration) []oidc.CodeChallengeMethod {
|
||||||
|
codeMethods := make([]oidc.CodeChallengeMethod, 0, 1)
|
||||||
if c.CodeMethodS256Supported() {
|
if c.CodeMethodS256Supported() {
|
||||||
codeMethods = append(codeMethods, CodeMethodS256)
|
codeMethods = append(codeMethods, oidc.CodeChallengeMethodS256)
|
||||||
}
|
}
|
||||||
return codeMethods
|
return codeMethods
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func UILocales(c Configuration) []language.Tag {
|
||||||
|
return []language.Tag{
|
||||||
|
language.English,
|
||||||
|
language.German,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -37,7 +37,7 @@ func TestDiscover(t *testing.T) {
|
||||||
op.Discover(tt.args.w, tt.args.config)
|
op.Discover(tt.args.w, tt.args.config)
|
||||||
rec := tt.args.w.(*httptest.ResponseRecorder)
|
rec := tt.args.w.(*httptest.ResponseRecorder)
|
||||||
require.Equal(t, http.StatusOK, rec.Code)
|
require.Equal(t, http.StatusOK, rec.Code)
|
||||||
require.Equal(t, `{"issuer":"https://issuer.com"}`, rec.Body.String())
|
require.Equal(t, `{"issuer":"https://issuer.com","request_uri_parameter_supported":false}`, rec.Body.String())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -199,36 +199,49 @@ func Test_SubjectTypes(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_AuthMethods(t *testing.T) {
|
func Test_AuthMethodsTokenEndpoint(t *testing.T) {
|
||||||
m := mock.NewMockConfiguration(gomock.NewController(t))
|
|
||||||
type args struct {
|
type args struct {
|
||||||
c op.Configuration
|
c op.Configuration
|
||||||
}
|
}
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
args args
|
args args
|
||||||
want []string
|
want []oidc.AuthMethod
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
"imlicit basic",
|
"none and basic",
|
||||||
args{func() op.Configuration {
|
args{func() op.Configuration {
|
||||||
|
m := mock.NewMockConfiguration(gomock.NewController(t))
|
||||||
m.EXPECT().AuthMethodPostSupported().Return(false)
|
m.EXPECT().AuthMethodPostSupported().Return(false)
|
||||||
|
m.EXPECT().AuthMethodPrivateKeyJWTSupported().Return(false)
|
||||||
return m
|
return m
|
||||||
}()},
|
}()},
|
||||||
[]string{string(op.AuthMethodBasic)},
|
[]oidc.AuthMethod{oidc.AuthMethodNone, oidc.AuthMethodBasic},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"basic and post",
|
"none, basic and post",
|
||||||
args{func() op.Configuration {
|
args{func() op.Configuration {
|
||||||
|
m := mock.NewMockConfiguration(gomock.NewController(t))
|
||||||
m.EXPECT().AuthMethodPostSupported().Return(true)
|
m.EXPECT().AuthMethodPostSupported().Return(true)
|
||||||
|
m.EXPECT().AuthMethodPrivateKeyJWTSupported().Return(false)
|
||||||
return m
|
return m
|
||||||
}()},
|
}()},
|
||||||
[]string{string(op.AuthMethodBasic), string(op.AuthMethodPost)},
|
[]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 {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
if got := op.AuthMethods(tt.args.c); !reflect.DeepEqual(got, tt.want) {
|
if got := op.AuthMethodsTokenEndpoint(tt.args.c); !reflect.DeepEqual(got, tt.want) {
|
||||||
t.Errorf("authMethods() = %v, want %v", got, tt.want)
|
t.Errorf("authMethods() = %v, want %v", got, tt.want)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -64,10 +64,10 @@ func (mr *MockClientMockRecorder) ApplicationType() *gomock.Call {
|
||||||
}
|
}
|
||||||
|
|
||||||
// AuthMethod mocks base method
|
// AuthMethod mocks base method
|
||||||
func (m *MockClient) AuthMethod() op.AuthMethod {
|
func (m *MockClient) AuthMethod() oidc.AuthMethod {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
ret := m.ctrl.Call(m, "AuthMethod")
|
ret := m.ctrl.Call(m, "AuthMethod")
|
||||||
ret0, _ := ret[0].(op.AuthMethod)
|
ret0, _ := ret[0].(oidc.AuthMethod)
|
||||||
return ret0
|
return ret0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -47,6 +47,20 @@ func (mr *MockConfigurationMockRecorder) AuthMethodPostSupported() *gomock.Call
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthMethodPostSupported", reflect.TypeOf((*MockConfiguration)(nil).AuthMethodPostSupported))
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthMethodPostSupported", reflect.TypeOf((*MockConfiguration)(nil).AuthMethodPostSupported))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AuthMethodPrivateKeyJWTSupported mocks base method
|
||||||
|
func (m *MockConfiguration) AuthMethodPrivateKeyJWTSupported() bool {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "AuthMethodPrivateKeyJWTSupported")
|
||||||
|
ret0, _ := ret[0].(bool)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthMethodPrivateKeyJWTSupported indicates an expected call of AuthMethodPrivateKeyJWTSupported
|
||||||
|
func (mr *MockConfigurationMockRecorder) AuthMethodPrivateKeyJWTSupported() *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthMethodPrivateKeyJWTSupported", reflect.TypeOf((*MockConfiguration)(nil).AuthMethodPrivateKeyJWTSupported))
|
||||||
|
}
|
||||||
|
|
||||||
// AuthorizationEndpoint mocks base method
|
// AuthorizationEndpoint mocks base method
|
||||||
func (m *MockConfiguration) AuthorizationEndpoint() op.Endpoint {
|
func (m *MockConfiguration) AuthorizationEndpoint() op.Endpoint {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
|
@ -117,6 +131,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))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IntrospectionEndpoint mocks base method
|
||||||
|
func (m *MockConfiguration) IntrospectionEndpoint() op.Endpoint {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "IntrospectionEndpoint")
|
||||||
|
ret0, _ := ret[0].(op.Endpoint)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// IntrospectionEndpoint indicates an expected call of IntrospectionEndpoint
|
||||||
|
func (mr *MockConfigurationMockRecorder) IntrospectionEndpoint() *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IntrospectionEndpoint", reflect.TypeOf((*MockConfiguration)(nil).IntrospectionEndpoint))
|
||||||
|
}
|
||||||
|
|
||||||
// Issuer mocks base method
|
// Issuer mocks base method
|
||||||
func (m *MockConfiguration) Issuer() string {
|
func (m *MockConfiguration) Issuer() string {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
|
|
|
@ -198,36 +198,6 @@ func (mr *MockStorageMockRecorder) GetSigningKey(arg0, arg1, arg2, arg3 interfac
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSigningKey", reflect.TypeOf((*MockStorage)(nil).GetSigningKey), arg0, arg1, arg2, arg3)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSigningKey", reflect.TypeOf((*MockStorage)(nil).GetSigningKey), arg0, arg1, arg2, arg3)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUserinfoFromScopes mocks base method
|
|
||||||
func (m *MockStorage) GetUserinfoFromScopes(arg0 context.Context, arg1, arg2 string, arg3 []string) (oidc.UserInfo, error) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "GetUserinfoFromScopes", arg0, arg1, arg2, arg3)
|
|
||||||
ret0, _ := ret[0].(oidc.UserInfo)
|
|
||||||
ret1, _ := ret[1].(error)
|
|
||||||
return ret0, ret1
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetUserinfoFromScopes indicates an expected call of GetUserinfoFromScopes
|
|
||||||
func (mr *MockStorageMockRecorder) GetUserinfoFromScopes(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserinfoFromScopes", reflect.TypeOf((*MockStorage)(nil).GetUserinfoFromScopes), arg0, arg1, arg2, arg3)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetUserinfoFromToken mocks base method
|
|
||||||
func (m *MockStorage) GetUserinfoFromToken(arg0 context.Context, arg1, arg2, arg3 string) (oidc.UserInfo, error) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "GetUserinfoFromToken", arg0, arg1, arg2, arg3)
|
|
||||||
ret0, _ := ret[0].(oidc.UserInfo)
|
|
||||||
ret1, _ := ret[1].(error)
|
|
||||||
return ret0, ret1
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetUserinfoFromToken indicates an expected call of GetUserinfoFromToken
|
|
||||||
func (mr *MockStorageMockRecorder) GetUserinfoFromToken(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserinfoFromToken", reflect.TypeOf((*MockStorage)(nil).GetUserinfoFromToken), arg0, arg1, arg2, arg3)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Health mocks base method
|
// Health mocks base method
|
||||||
func (m *MockStorage) Health(arg0 context.Context) error {
|
func (m *MockStorage) Health(arg0 context.Context) error {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
|
@ -270,6 +240,48 @@ func (mr *MockStorageMockRecorder) SaveNewKeyPair(arg0 interface{}) *gomock.Call
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveNewKeyPair", reflect.TypeOf((*MockStorage)(nil).SaveNewKeyPair), arg0)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveNewKeyPair", reflect.TypeOf((*MockStorage)(nil).SaveNewKeyPair), arg0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetIntrospectionFromToken mocks base method
|
||||||
|
func (m *MockStorage) SetIntrospectionFromToken(arg0 context.Context, arg1 oidc.IntrospectionResponse, arg2, arg3, arg4 string) error {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "SetIntrospectionFromToken", arg0, arg1, arg2, arg3, arg4)
|
||||||
|
ret0, _ := ret[0].(error)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetIntrospectionFromToken indicates an expected call of SetIntrospectionFromToken
|
||||||
|
func (mr *MockStorageMockRecorder) SetIntrospectionFromToken(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetIntrospectionFromToken", reflect.TypeOf((*MockStorage)(nil).SetIntrospectionFromToken), arg0, arg1, arg2, arg3, arg4)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetUserinfoFromScopes mocks base method
|
||||||
|
func (m *MockStorage) SetUserinfoFromScopes(arg0 context.Context, arg1 oidc.UserInfoSetter, arg2, arg3 string, arg4 []string) error {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "SetUserinfoFromScopes", arg0, arg1, arg2, arg3, arg4)
|
||||||
|
ret0, _ := ret[0].(error)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetUserinfoFromScopes indicates an expected call of SetUserinfoFromScopes
|
||||||
|
func (mr *MockStorageMockRecorder) SetUserinfoFromScopes(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetUserinfoFromScopes", reflect.TypeOf((*MockStorage)(nil).SetUserinfoFromScopes), arg0, arg1, arg2, arg3, arg4)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetUserinfoFromToken mocks base method
|
||||||
|
func (m *MockStorage) SetUserinfoFromToken(arg0 context.Context, arg1 oidc.UserInfoSetter, arg2, arg3, arg4 string) error {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "SetUserinfoFromToken", arg0, arg1, arg2, arg3, arg4)
|
||||||
|
ret0, _ := ret[0].(error)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetUserinfoFromToken indicates an expected call of SetUserinfoFromToken
|
||||||
|
func (mr *MockStorageMockRecorder) SetUserinfoFromToken(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetUserinfoFromToken", reflect.TypeOf((*MockStorage)(nil).SetUserinfoFromToken), arg0, arg1, arg2, arg3, arg4)
|
||||||
|
}
|
||||||
|
|
||||||
// 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()
|
||||||
|
@ -283,3 +295,18 @@ func (mr *MockStorageMockRecorder) TerminateSession(arg0, arg1, arg2 interface{}
|
||||||
mr.mock.ctrl.T.Helper()
|
mr.mock.ctrl.T.Helper()
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TerminateSession", reflect.TypeOf((*MockStorage)(nil).TerminateSession), arg0, arg1, arg2)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TerminateSession", reflect.TypeOf((*MockStorage)(nil).TerminateSession), arg0, arg1, arg2)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ValidateJWTProfileScopes mocks base method
|
||||||
|
func (m *MockStorage) ValidateJWTProfileScopes(arg0 context.Context, arg1 string, arg2 oidc.Scopes) (oidc.Scopes, error) {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "ValidateJWTProfileScopes", arg0, arg1, arg2)
|
||||||
|
ret0, _ := ret[0].(oidc.Scopes)
|
||||||
|
ret1, _ := ret[1].(error)
|
||||||
|
return ret0, ret1
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateJWTProfileScopes indicates an expected call of ValidateJWTProfileScopes
|
||||||
|
func (mr *MockStorageMockRecorder) ValidateJWTProfileScopes(arg0, arg1, arg2 interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ValidateJWTProfileScopes", reflect.TypeOf((*MockStorage)(nil).ValidateJWTProfileScopes), arg0, arg1, arg2)
|
||||||
|
}
|
||||||
|
|
|
@ -65,23 +65,23 @@ func ExpectValidClientID(s op.Storage) {
|
||||||
mockS.EXPECT().GetClientByClientID(gomock.Any(), gomock.Any()).DoAndReturn(
|
mockS.EXPECT().GetClientByClientID(gomock.Any(), gomock.Any()).DoAndReturn(
|
||||||
func(_ context.Context, id string) (op.Client, error) {
|
func(_ context.Context, id string) (op.Client, error) {
|
||||||
var appType op.ApplicationType
|
var appType op.ApplicationType
|
||||||
var authMethod op.AuthMethod
|
var authMethod oidc.AuthMethod
|
||||||
var accessTokenType op.AccessTokenType
|
var accessTokenType op.AccessTokenType
|
||||||
var responseTypes []oidc.ResponseType
|
var responseTypes []oidc.ResponseType
|
||||||
switch id {
|
switch id {
|
||||||
case "web_client":
|
case "web_client":
|
||||||
appType = op.ApplicationTypeWeb
|
appType = op.ApplicationTypeWeb
|
||||||
authMethod = op.AuthMethodBasic
|
authMethod = oidc.AuthMethodBasic
|
||||||
accessTokenType = op.AccessTokenTypeBearer
|
accessTokenType = op.AccessTokenTypeBearer
|
||||||
responseTypes = []oidc.ResponseType{oidc.ResponseTypeCode}
|
responseTypes = []oidc.ResponseType{oidc.ResponseTypeCode}
|
||||||
case "native_client":
|
case "native_client":
|
||||||
appType = op.ApplicationTypeNative
|
appType = op.ApplicationTypeNative
|
||||||
authMethod = op.AuthMethodNone
|
authMethod = oidc.AuthMethodNone
|
||||||
accessTokenType = op.AccessTokenTypeBearer
|
accessTokenType = op.AccessTokenTypeBearer
|
||||||
responseTypes = []oidc.ResponseType{oidc.ResponseTypeCode}
|
responseTypes = []oidc.ResponseType{oidc.ResponseTypeCode}
|
||||||
case "useragent_client":
|
case "useragent_client":
|
||||||
appType = op.ApplicationTypeUserAgent
|
appType = op.ApplicationTypeUserAgent
|
||||||
authMethod = op.AuthMethodBasic
|
authMethod = oidc.AuthMethodBasic
|
||||||
accessTokenType = op.AccessTokenTypeJWT
|
accessTokenType = op.AccessTokenTypeJWT
|
||||||
responseTypes = []oidc.ResponseType{oidc.ResponseTypeIDToken}
|
responseTypes = []oidc.ResponseType{oidc.ResponseTypeIDToken}
|
||||||
}
|
}
|
||||||
|
@ -119,7 +119,7 @@ func ExpectSigningKey(s op.Storage) {
|
||||||
type ConfClient struct {
|
type ConfClient struct {
|
||||||
id string
|
id string
|
||||||
appType op.ApplicationType
|
appType op.ApplicationType
|
||||||
authMethod op.AuthMethod
|
authMethod oidc.AuthMethod
|
||||||
accessTokenType op.AccessTokenType
|
accessTokenType op.AccessTokenType
|
||||||
responseTypes []oidc.ResponseType
|
responseTypes []oidc.ResponseType
|
||||||
devMode bool
|
devMode bool
|
||||||
|
@ -145,7 +145,7 @@ func (c *ConfClient) ApplicationType() op.ApplicationType {
|
||||||
return c.appType
|
return c.appType
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ConfClient) AuthMethod() op.AuthMethod {
|
func (c *ConfClient) AuthMethod() oidc.AuthMethod {
|
||||||
return c.authMethod
|
return c.authMethod
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
36
pkg/op/op.go
36
pkg/op/op.go
|
@ -17,26 +17,20 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
healthzEndpoint = "/healthz"
|
healthEndpoint = "/healthz"
|
||||||
readinessEndpoint = "/ready"
|
readinessEndpoint = "/ready"
|
||||||
defaultAuthorizationEndpoint = "authorize"
|
defaultAuthorizationEndpoint = "authorize"
|
||||||
defaulTokenEndpoint = "oauth/token"
|
defaultTokenEndpoint = "oauth/token"
|
||||||
defaultIntrospectEndpoint = "introspect"
|
defaultIntrospectEndpoint = "oauth/introspect"
|
||||||
defaultUserinfoEndpoint = "userinfo"
|
defaultUserinfoEndpoint = "userinfo"
|
||||||
defaultEndSessionEndpoint = "end_session"
|
defaultEndSessionEndpoint = "end_session"
|
||||||
defaultKeysEndpoint = "keys"
|
defaultKeysEndpoint = "keys"
|
||||||
|
|
||||||
AuthMethodBasic AuthMethod = "client_secret_basic"
|
|
||||||
AuthMethodPost AuthMethod = "client_secret_post"
|
|
||||||
AuthMethodNone AuthMethod = "none"
|
|
||||||
|
|
||||||
CodeMethodS256 = "S256"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
DefaultEndpoints = &endpoints{
|
DefaultEndpoints = &endpoints{
|
||||||
Authorization: NewEndpoint(defaultAuthorizationEndpoint),
|
Authorization: NewEndpoint(defaultAuthorizationEndpoint),
|
||||||
Token: NewEndpoint(defaulTokenEndpoint),
|
Token: NewEndpoint(defaultTokenEndpoint),
|
||||||
Introspection: NewEndpoint(defaultIntrospectEndpoint),
|
Introspection: NewEndpoint(defaultIntrospectEndpoint),
|
||||||
Userinfo: NewEndpoint(defaultUserinfoEndpoint),
|
Userinfo: NewEndpoint(defaultUserinfoEndpoint),
|
||||||
EndSession: NewEndpoint(defaultEndSessionEndpoint),
|
EndSession: NewEndpoint(defaultEndSessionEndpoint),
|
||||||
|
@ -72,12 +66,13 @@ func CreateRouter(o OpenIDProvider, interceptors ...HttpInterceptor) *mux.Router
|
||||||
handlers.AllowedHeaders([]string{"authorization", "content-type"}),
|
handlers.AllowedHeaders([]string{"authorization", "content-type"}),
|
||||||
handlers.AllowedOriginValidator(allowAllOrigins),
|
handlers.AllowedOriginValidator(allowAllOrigins),
|
||||||
))
|
))
|
||||||
router.HandleFunc(healthzEndpoint, healthzHandler)
|
router.HandleFunc(healthEndpoint, healthHandler)
|
||||||
router.HandleFunc(readinessEndpoint, readyHandler(o.Probes()))
|
router.HandleFunc(readinessEndpoint, readyHandler(o.Probes()))
|
||||||
router.HandleFunc(oidc.DiscoveryEndpoint, discoveryHandler(o, o.Signer()))
|
router.HandleFunc(oidc.DiscoveryEndpoint, discoveryHandler(o, o.Signer()))
|
||||||
router.Handle(o.AuthorizationEndpoint().Relative(), intercept(authorizeHandler(o)))
|
router.Handle(o.AuthorizationEndpoint().Relative(), intercept(authorizeHandler(o)))
|
||||||
router.NewRoute().Path(o.AuthorizationEndpoint().Relative()+"/callback").Queries("id", "{id}").Handler(intercept(authorizeCallbackHandler(o)))
|
router.NewRoute().Path(o.AuthorizationEndpoint().Relative()+"/callback").Queries("id", "{id}").Handler(intercept(authorizeCallbackHandler(o)))
|
||||||
router.Handle(o.TokenEndpoint().Relative(), intercept(tokenHandler(o)))
|
router.Handle(o.TokenEndpoint().Relative(), intercept(tokenHandler(o)))
|
||||||
|
router.HandleFunc(o.IntrospectionEndpoint().Relative(), introspectionHandler(o))
|
||||||
router.HandleFunc(o.UserinfoEndpoint().Relative(), userinfoHandler(o))
|
router.HandleFunc(o.UserinfoEndpoint().Relative(), userinfoHandler(o))
|
||||||
router.Handle(o.EndSessionEndpoint().Relative(), intercept(endSessionHandler(o)))
|
router.Handle(o.EndSessionEndpoint().Relative(), intercept(endSessionHandler(o)))
|
||||||
router.HandleFunc(o.KeysEndpoint().Relative(), keysHandler(o))
|
router.HandleFunc(o.KeysEndpoint().Relative(), keysHandler(o))
|
||||||
|
@ -89,6 +84,7 @@ type Config struct {
|
||||||
CryptoKey [32]byte
|
CryptoKey [32]byte
|
||||||
DefaultLogoutRedirectURI string
|
DefaultLogoutRedirectURI string
|
||||||
CodeMethodS256 bool
|
CodeMethodS256 bool
|
||||||
|
AuthMethodPrivateKeyJWT bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type endpoints struct {
|
type endpoints struct {
|
||||||
|
@ -166,6 +162,10 @@ func (o *openidProvider) TokenEndpoint() Endpoint {
|
||||||
return o.endpoints.Token
|
return o.endpoints.Token
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (o *openidProvider) IntrospectionEndpoint() Endpoint {
|
||||||
|
return o.endpoints.Introspection
|
||||||
|
}
|
||||||
|
|
||||||
func (o *openidProvider) UserinfoEndpoint() Endpoint {
|
func (o *openidProvider) UserinfoEndpoint() Endpoint {
|
||||||
return o.endpoints.Userinfo
|
return o.endpoints.Userinfo
|
||||||
}
|
}
|
||||||
|
@ -186,6 +186,10 @@ func (o *openidProvider) CodeMethodS256Supported() bool {
|
||||||
return o.config.CodeMethodS256
|
return o.config.CodeMethodS256
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (o *openidProvider) AuthMethodPrivateKeyJWTSupported() bool {
|
||||||
|
return o.config.AuthMethodPrivateKeyJWT
|
||||||
|
}
|
||||||
|
|
||||||
func (o *openidProvider) GrantTypeTokenExchangeSupported() bool {
|
func (o *openidProvider) GrantTypeTokenExchangeSupported() bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -332,6 +336,16 @@ func WithCustomTokenEndpoint(endpoint Endpoint) Option {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func WithCustomIntrospectionEndpoint(endpoint Endpoint) Option {
|
||||||
|
return func(o *openidProvider) error {
|
||||||
|
if err := endpoint.Validate(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
o.endpoints.Introspection = endpoint
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func WithCustomUserinfoEndpoint(endpoint Endpoint) Option {
|
func WithCustomUserinfoEndpoint(endpoint Endpoint) Option {
|
||||||
return func(o *openidProvider) error {
|
return func(o *openidProvider) error {
|
||||||
if err := endpoint.Validate(); err != nil {
|
if err := endpoint.Validate(); err != nil {
|
||||||
|
|
|
@ -10,7 +10,7 @@ import (
|
||||||
|
|
||||||
type ProbesFn func(context.Context) error
|
type ProbesFn func(context.Context) error
|
||||||
|
|
||||||
func healthzHandler(w http.ResponseWriter, r *http.Request) {
|
func healthHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
ok(w)
|
ok(w)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -28,10 +28,12 @@ type AuthStorage interface {
|
||||||
type OPStorage interface {
|
type OPStorage interface {
|
||||||
GetClientByClientID(ctx context.Context, clientID string) (Client, error)
|
GetClientByClientID(ctx context.Context, clientID string) (Client, error)
|
||||||
AuthorizeClientIDSecret(ctx context.Context, clientID, clientSecret string) error
|
AuthorizeClientIDSecret(ctx context.Context, clientID, clientSecret string) error
|
||||||
GetUserinfoFromScopes(ctx context.Context, userID, clientID string, scopes []string) (oidc.UserInfo, error)
|
SetUserinfoFromScopes(ctx context.Context, userinfo oidc.UserInfoSetter, userID, clientID string, scopes []string) error
|
||||||
GetUserinfoFromToken(ctx context.Context, tokenID, subject, origin string) (oidc.UserInfo, error)
|
SetUserinfoFromToken(ctx context.Context, userinfo oidc.UserInfoSetter, tokenID, subject, origin string) error
|
||||||
|
SetIntrospectionFromToken(ctx context.Context, userinfo oidc.IntrospectionResponse, tokenID, subject, clientID string) error
|
||||||
GetPrivateClaimsFromScopes(ctx context.Context, userID, clientID string, scopes []string) (map[string]interface{}, error)
|
GetPrivateClaimsFromScopes(ctx context.Context, userID, clientID string, scopes []string) (map[string]interface{}, error)
|
||||||
GetKeyByIDAndUserID(ctx context.Context, keyID, userID string) (*jose.JSONWebKey, error)
|
GetKeyByIDAndUserID(ctx context.Context, keyID, userID string) (*jose.JSONWebKey, error)
|
||||||
|
ValidateJWTProfileScopes(ctx context.Context, userID string, scope oidc.Scopes) (oidc.Scopes, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type Storage interface {
|
type Storage interface {
|
||||||
|
@ -44,23 +46,6 @@ type StorageNotFoundError interface {
|
||||||
IsNotFound()
|
IsNotFound()
|
||||||
}
|
}
|
||||||
|
|
||||||
type AuthRequest interface {
|
|
||||||
GetID() string
|
|
||||||
GetACR() string
|
|
||||||
GetAMR() []string
|
|
||||||
GetAudience() []string
|
|
||||||
GetAuthTime() time.Time
|
|
||||||
GetClientID() string
|
|
||||||
GetCodeChallenge() *oidc.CodeChallenge
|
|
||||||
GetNonce() string
|
|
||||||
GetRedirectURI() string
|
|
||||||
GetResponseType() oidc.ResponseType
|
|
||||||
GetScopes() []string
|
|
||||||
GetState() string
|
|
||||||
GetSubject() string
|
|
||||||
Done() bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type EndSessionRequest struct {
|
type EndSessionRequest struct {
|
||||||
UserID string
|
UserID string
|
||||||
Client Client
|
Client Client
|
||||||
|
|
|
@ -114,7 +114,8 @@ func CreateIDToken(ctx context.Context, issuer string, authReq AuthRequest, vali
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(scopes) > 0 {
|
if len(scopes) > 0 {
|
||||||
userInfo, err := storage.GetUserinfoFromScopes(ctx, authReq.GetSubject(), authReq.GetClientID(), scopes)
|
userInfo := oidc.NewUserInfo()
|
||||||
|
err := storage.SetUserinfoFromScopes(ctx, userInfo, authReq.GetSubject(), authReq.GetClientID(), scopes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
77
pkg/op/token_intospection.go
Normal file
77
pkg/op/token_intospection.go
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
package op
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/caos/oidc/pkg/oidc"
|
||||||
|
"github.com/caos/oidc/pkg/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Introspector interface {
|
||||||
|
Decoder() utils.Decoder
|
||||||
|
Crypto() Crypto
|
||||||
|
Storage() Storage
|
||||||
|
AccessTokenVerifier() AccessTokenVerifier
|
||||||
|
}
|
||||||
|
|
||||||
|
type IntrospectorJWTProfile interface {
|
||||||
|
Introspector
|
||||||
|
JWTProfileVerifier() JWTProfileVerifier
|
||||||
|
}
|
||||||
|
|
||||||
|
func introspectionHandler(introspector Introspector) func(http.ResponseWriter, *http.Request) {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
Introspect(w, r, introspector)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Introspect(w http.ResponseWriter, r *http.Request, introspector Introspector) {
|
||||||
|
response := oidc.NewIntrospectionResponse()
|
||||||
|
token, clientID, err := ParseTokenIntrospectionRequest(r, introspector)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tokenID, subject, ok := getTokenIDAndSubject(r.Context(), introspector, token)
|
||||||
|
if !ok {
|
||||||
|
utils.MarshalJSON(w, response)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = introspector.Storage().SetIntrospectionFromToken(r.Context(), response, tokenID, subject, clientID)
|
||||||
|
if err != nil {
|
||||||
|
utils.MarshalJSON(w, response)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response.SetActive(true)
|
||||||
|
utils.MarshalJSON(w, response)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseTokenIntrospectionRequest(r *http.Request, introspector Introspector) (token, clientID string, err error) {
|
||||||
|
err = r.ParseForm()
|
||||||
|
if err != nil {
|
||||||
|
return "", "", errors.New("unable to parse request")
|
||||||
|
}
|
||||||
|
req := new(struct {
|
||||||
|
oidc.IntrospectionRequest
|
||||||
|
oidc.ClientAssertionParams
|
||||||
|
})
|
||||||
|
err = introspector.Decoder().Decode(req, r.Form)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", errors.New("unable to parse request")
|
||||||
|
}
|
||||||
|
if introspectorJWTProfile, ok := introspector.(IntrospectorJWTProfile); ok && req.ClientAssertion != "" {
|
||||||
|
profile, err := VerifyJWTAssertion(r.Context(), req.ClientAssertion, introspectorJWTProfile.JWTProfileVerifier())
|
||||||
|
if err == nil {
|
||||||
|
return req.Token, profile.Issuer, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
clientID, clientSecret, ok := r.BasicAuth()
|
||||||
|
if ok {
|
||||||
|
if err := introspector.Storage().AuthorizeClientIDSecret(r.Context(), clientID, clientSecret); err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
return req.Token, clientID, nil
|
||||||
|
}
|
||||||
|
return "", "", errors.New("invalid authorization")
|
||||||
|
}
|
|
@ -7,7 +7,6 @@ import (
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
"github.com/caos/oidc/pkg/oidc"
|
"github.com/caos/oidc/pkg/oidc"
|
||||||
"github.com/caos/oidc/pkg/oidc/grants/tokenexchange"
|
|
||||||
"github.com/caos/oidc/pkg/utils"
|
"github.com/caos/oidc/pkg/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -18,6 +17,7 @@ type Exchanger interface {
|
||||||
Signer() Signer
|
Signer() Signer
|
||||||
Crypto() Crypto
|
Crypto() Crypto
|
||||||
AuthMethodPostSupported() bool
|
AuthMethodPostSupported() bool
|
||||||
|
AuthMethodPrivateKeyJWTSupported() bool
|
||||||
GrantTypeTokenExchangeSupported() bool
|
GrantTypeTokenExchangeSupported() bool
|
||||||
GrantTypeJWTAuthorizationSupported() bool
|
GrantTypeJWTAuthorizationSupported() bool
|
||||||
}
|
}
|
||||||
|
@ -112,18 +112,33 @@ func ValidateAccessTokenRequest(ctx context.Context, tokenReq *oidc.AccessTokenR
|
||||||
}
|
}
|
||||||
|
|
||||||
func AuthorizeClient(ctx context.Context, tokenReq *oidc.AccessTokenRequest, exchanger Exchanger) (AuthRequest, Client, error) {
|
func AuthorizeClient(ctx context.Context, tokenReq *oidc.AccessTokenRequest, exchanger Exchanger) (AuthRequest, Client, error) {
|
||||||
|
if tokenReq.ClientAssertionType == oidc.ClientAssertionTypeJWTAssertion {
|
||||||
|
jwtExchanger, ok := exchanger.(JWTAuthorizationGrantExchanger)
|
||||||
|
if !ok || !exchanger.AuthMethodPrivateKeyJWTSupported() {
|
||||||
|
return nil, nil, errors.New("auth_method private_key_jwt not supported")
|
||||||
|
}
|
||||||
|
return AuthorizePrivateJWTKey(ctx, tokenReq, jwtExchanger)
|
||||||
|
}
|
||||||
client, err := exchanger.Storage().GetClientByClientID(ctx, tokenReq.ClientID)
|
client, err := exchanger.Storage().GetClientByClientID(ctx, tokenReq.ClientID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
if client.AuthMethod() == AuthMethodNone {
|
if client.AuthMethod() == oidc.AuthMethodPrivateKeyJWT {
|
||||||
|
return nil, nil, errors.New("invalid_grant")
|
||||||
|
}
|
||||||
|
if client.AuthMethod() == oidc.AuthMethodNone {
|
||||||
authReq, err := AuthorizeCodeChallenge(ctx, tokenReq, exchanger)
|
authReq, err := AuthorizeCodeChallenge(ctx, tokenReq, exchanger)
|
||||||
return authReq, client, err
|
return authReq, client, err
|
||||||
}
|
}
|
||||||
if client.AuthMethod() == AuthMethodPost && !exchanger.AuthMethodPostSupported() {
|
if client.AuthMethod() == oidc.AuthMethodPost && !exchanger.AuthMethodPostSupported() {
|
||||||
return nil, nil, errors.New("auth_method post not supported")
|
return nil, nil, errors.New("auth_method post not supported")
|
||||||
}
|
}
|
||||||
err = AuthorizeClientIDSecret(ctx, tokenReq.ClientID, tokenReq.ClientSecret, exchanger.Storage())
|
authReq, err := AuthorizeClientIDSecret(ctx, tokenReq.ClientID, tokenReq.ClientSecret, tokenReq.Code, exchanger.Storage())
|
||||||
|
return authReq, client, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func AuthorizePrivateJWTKey(ctx context.Context, tokenReq *oidc.AccessTokenRequest, exchanger JWTAuthorizationGrantExchanger) (AuthRequest, Client, error) {
|
||||||
|
jwtReq, err := VerifyJWTAssertion(ctx, tokenReq.ClientAssertion, exchanger.JWTProfileVerifier())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
@ -131,11 +146,26 @@ func AuthorizeClient(ctx context.Context, tokenReq *oidc.AccessTokenRequest, exc
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, ErrInvalidRequest("invalid code")
|
return nil, nil, ErrInvalidRequest("invalid code")
|
||||||
}
|
}
|
||||||
|
client, err := exchanger.Storage().GetClientByClientID(ctx, jwtReq.Issuer)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
if client.AuthMethod() != oidc.AuthMethodPrivateKeyJWT {
|
||||||
|
return nil, nil, ErrInvalidRequest("invalid_client")
|
||||||
|
}
|
||||||
return authReq, client, nil
|
return authReq, client, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func AuthorizeClientIDSecret(ctx context.Context, clientID, clientSecret string, storage OPStorage) error {
|
func AuthorizeClientIDSecret(ctx context.Context, clientID, clientSecret, code string, storage Storage) (AuthRequest, error) {
|
||||||
return storage.AuthorizeClientIDSecret(ctx, clientID, clientSecret)
|
err := storage.AuthorizeClientIDSecret(ctx, clientID, clientSecret)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
authReq, err := storage.AuthRequestByCode(ctx, code)
|
||||||
|
if err != nil {
|
||||||
|
return nil, ErrInvalidRequest("invalid code")
|
||||||
|
}
|
||||||
|
return authReq, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func AuthorizeCodeChallenge(ctx context.Context, tokenReq *oidc.AccessTokenRequest, exchanger Exchanger) (AuthRequest, error) {
|
func AuthorizeCodeChallenge(ctx context.Context, tokenReq *oidc.AccessTokenRequest, exchanger Exchanger) (AuthRequest, error) {
|
||||||
|
@ -158,12 +188,17 @@ func JWTProfile(w http.ResponseWriter, r *http.Request, exchanger JWTAuthorizati
|
||||||
RequestError(w, r, err)
|
RequestError(w, r, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
tokenRequest, err := VerifyJWTAssertion(r.Context(), profileRequest, exchanger.JWTProfileVerifier())
|
tokenRequest, err := VerifyJWTAssertion(r.Context(), profileRequest.Assertion, exchanger.JWTProfileVerifier())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
RequestError(w, r, err)
|
RequestError(w, r, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tokenRequest.Scopes, err = exchanger.Storage().ValidateJWTProfileScopes(r.Context(), tokenRequest.Issuer, profileRequest.Scope)
|
||||||
|
if err != nil {
|
||||||
|
RequestError(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
resp, err := CreateJWTTokenResponse(r.Context(), tokenRequest, exchanger)
|
resp, err := CreateJWTTokenResponse(r.Context(), tokenRequest, exchanger)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
RequestError(w, r, err)
|
RequestError(w, r, err)
|
||||||
|
@ -172,12 +207,12 @@ func JWTProfile(w http.ResponseWriter, r *http.Request, exchanger JWTAuthorizati
|
||||||
utils.MarshalJSON(w, resp)
|
utils.MarshalJSON(w, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
func ParseJWTProfileRequest(r *http.Request, decoder utils.Decoder) (*tokenexchange.JWTProfileRequest, error) {
|
func ParseJWTProfileRequest(r *http.Request, decoder utils.Decoder) (*oidc.JWTProfileGrantRequest, error) {
|
||||||
err := r.ParseForm()
|
err := r.ParseForm()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, ErrInvalidRequest("error parsing form")
|
return nil, ErrInvalidRequest("error parsing form")
|
||||||
}
|
}
|
||||||
tokenReq := new(tokenexchange.JWTProfileRequest)
|
tokenReq := new(oidc.JWTProfileGrantRequest)
|
||||||
err = decoder.Decode(tokenReq, r.Form)
|
err = decoder.Decode(tokenReq, r.Form)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, ErrInvalidRequest("error decoding form")
|
return nil, ErrInvalidRequest("error decoding form")
|
||||||
|
|
|
@ -24,7 +24,7 @@ func userinfoHandler(userinfoProvider UserinfoProvider) func(http.ResponseWriter
|
||||||
}
|
}
|
||||||
|
|
||||||
func Userinfo(w http.ResponseWriter, r *http.Request, userinfoProvider UserinfoProvider) {
|
func Userinfo(w http.ResponseWriter, r *http.Request, userinfoProvider UserinfoProvider) {
|
||||||
accessToken, err := getAccessToken(r, userinfoProvider.Decoder())
|
accessToken, err := ParseUserinfoRequest(r, userinfoProvider.Decoder())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "access token missing", http.StatusUnauthorized)
|
http.Error(w, "access token missing", http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
|
@ -34,7 +34,8 @@ func Userinfo(w http.ResponseWriter, r *http.Request, userinfoProvider UserinfoP
|
||||||
http.Error(w, "access token invalid", http.StatusUnauthorized)
|
http.Error(w, "access token invalid", http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
info, err := userinfoProvider.Storage().GetUserinfoFromToken(r.Context(), tokenID, subject, r.Header.Get("origin"))
|
info := oidc.NewUserInfo()
|
||||||
|
err = userinfoProvider.Storage().SetUserinfoFromToken(r.Context(), info, tokenID, subject, r.Header.Get("origin"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.WriteHeader(http.StatusForbidden)
|
w.WriteHeader(http.StatusForbidden)
|
||||||
utils.MarshalJSON(w, err)
|
utils.MarshalJSON(w, err)
|
||||||
|
@ -43,16 +44,12 @@ func Userinfo(w http.ResponseWriter, r *http.Request, userinfoProvider UserinfoP
|
||||||
utils.MarshalJSON(w, info)
|
utils.MarshalJSON(w, info)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getAccessToken(r *http.Request, decoder utils.Decoder) (string, error) {
|
func ParseUserinfoRequest(r *http.Request, decoder utils.Decoder) (string, error) {
|
||||||
authHeader := r.Header.Get("authorization")
|
accessToken, err := getAccessToken(r)
|
||||||
if authHeader != "" {
|
if err == nil {
|
||||||
parts := strings.Split(authHeader, "Bearer ")
|
return accessToken, nil
|
||||||
if len(parts) != 2 {
|
|
||||||
return "", errors.New("invalid auth header")
|
|
||||||
}
|
|
||||||
return parts[1], nil
|
|
||||||
}
|
}
|
||||||
err := r.ParseForm()
|
err = r.ParseForm()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", errors.New("unable to parse request")
|
return "", errors.New("unable to parse request")
|
||||||
}
|
}
|
||||||
|
@ -64,6 +61,18 @@ func getAccessToken(r *http.Request, decoder utils.Decoder) (string, error) {
|
||||||
return req.AccessToken, nil
|
return req.AccessToken, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getAccessToken(r *http.Request) (string, error) {
|
||||||
|
authHeader := r.Header.Get("authorization")
|
||||||
|
if authHeader == "" {
|
||||||
|
return "", errors.New("no auth header")
|
||||||
|
}
|
||||||
|
parts := strings.Split(authHeader, "Bearer ")
|
||||||
|
if len(parts) != 2 {
|
||||||
|
return "", errors.New("invalid auth header")
|
||||||
|
}
|
||||||
|
return parts[1], nil
|
||||||
|
}
|
||||||
|
|
||||||
func getTokenIDAndSubject(ctx context.Context, userinfoProvider UserinfoProvider, accessToken string) (string, string, bool) {
|
func getTokenIDAndSubject(ctx context.Context, userinfoProvider UserinfoProvider, accessToken string) (string, string, bool) {
|
||||||
tokenIDSubject, err := userinfoProvider.Crypto().Decrypt(accessToken)
|
tokenIDSubject, err := userinfoProvider.Crypto().Decrypt(accessToken)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
|
|
@ -8,7 +8,6 @@ import (
|
||||||
"gopkg.in/square/go-jose.v2"
|
"gopkg.in/square/go-jose.v2"
|
||||||
|
|
||||||
"github.com/caos/oidc/pkg/oidc"
|
"github.com/caos/oidc/pkg/oidc"
|
||||||
"github.com/caos/oidc/pkg/oidc/grants/tokenexchange"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type JWTProfileVerifier interface {
|
type JWTProfileVerifier interface {
|
||||||
|
@ -48,9 +47,9 @@ func (v *jwtProfileVerifier) Offset() time.Duration {
|
||||||
return v.offset
|
return v.offset
|
||||||
}
|
}
|
||||||
|
|
||||||
func VerifyJWTAssertion(ctx context.Context, profileRequest *tokenexchange.JWTProfileRequest, v JWTProfileVerifier) (*oidc.JWTTokenRequest, error) {
|
func VerifyJWTAssertion(ctx context.Context, assertion string, v JWTProfileVerifier) (*oidc.JWTTokenRequest, error) {
|
||||||
request := new(oidc.JWTTokenRequest)
|
request := new(oidc.JWTTokenRequest)
|
||||||
payload, err := oidc.ParseToken(profileRequest.Assertion, request)
|
payload, err := oidc.ParseToken(assertion, request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -71,12 +70,11 @@ func VerifyJWTAssertion(ctx context.Context, profileRequest *tokenexchange.JWTPr
|
||||||
//TODO: implement delegation (openid core / oauth rfc)
|
//TODO: implement delegation (openid core / oauth rfc)
|
||||||
}
|
}
|
||||||
|
|
||||||
keySet := &jwtProfileKeySet{v.Storage(), request.Subject}
|
keySet := &jwtProfileKeySet{v.Storage(), request.Issuer}
|
||||||
|
|
||||||
if err = oidc.CheckSignature(ctx, profileRequest.Assertion, payload, request, nil, keySet); err != nil {
|
if err = oidc.CheckSignature(ctx, assertion, payload, request, nil, keySet); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
request.Scopes = profileRequest.Scope
|
|
||||||
return request, nil
|
return request, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,100 +0,0 @@
|
||||||
package rp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto/rsa"
|
|
||||||
"crypto/x509"
|
|
||||||
"encoding/json"
|
|
||||||
"encoding/pem"
|
|
||||||
|
|
||||||
"golang.org/x/oauth2"
|
|
||||||
"gopkg.in/square/go-jose.v2"
|
|
||||||
|
|
||||||
"github.com/caos/oidc/pkg/oidc"
|
|
||||||
"github.com/caos/oidc/pkg/oidc/grants/tokenexchange"
|
|
||||||
)
|
|
||||||
|
|
||||||
//TokenExchangeRP extends the `RelayingParty` interface for the *draft* oauth2 `Token Exchange`
|
|
||||||
type TokenExchangeRP interface {
|
|
||||||
RelayingParty
|
|
||||||
|
|
||||||
//TokenExchange implement the `Token Exchange Grant` exchanging some token for an other
|
|
||||||
TokenExchange(context.Context, *tokenexchange.TokenExchangeRequest) (*oauth2.Token, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
//DelegationTokenExchangeRP extends the `TokenExchangeRP` interface
|
|
||||||
//for the specific `delegation token` request
|
|
||||||
type DelegationTokenExchangeRP interface {
|
|
||||||
TokenExchangeRP
|
|
||||||
|
|
||||||
//DelegationTokenExchange implement the `Token Exchange Grant`
|
|
||||||
//providing an access token in request for a `delegation` token for a given resource / audience
|
|
||||||
DelegationTokenExchange(context.Context, string, ...tokenexchange.TokenExchangeOption) (*oauth2.Token, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
//TokenExchange handles the oauth2 token exchange
|
|
||||||
func TokenExchange(ctx context.Context, request *tokenexchange.TokenExchangeRequest, rp RelayingParty) (newToken *oauth2.Token, err error) {
|
|
||||||
return CallTokenEndpoint(request, rp)
|
|
||||||
}
|
|
||||||
|
|
||||||
//DelegationTokenExchange handles the oauth2 token exchange for a delegation token
|
|
||||||
func DelegationTokenExchange(ctx context.Context, subjectToken string, rp RelayingParty, reqOpts ...tokenexchange.TokenExchangeOption) (newToken *oauth2.Token, err error) {
|
|
||||||
return TokenExchange(ctx, DelegationTokenRequest(subjectToken, reqOpts...), rp)
|
|
||||||
}
|
|
||||||
|
|
||||||
//JWTProfileExchange handles the oauth2 jwt profile exchange
|
|
||||||
func JWTProfileExchange(ctx context.Context, jwtProfileRequest *tokenexchange.JWTProfileRequest, rp RelayingParty) (*oauth2.Token, error) {
|
|
||||||
return CallTokenEndpoint(jwtProfileRequest, rp)
|
|
||||||
}
|
|
||||||
|
|
||||||
//JWTProfileExchange handles the oauth2 jwt profile exchange
|
|
||||||
func JWTProfileAssertionExchange(ctx context.Context, assertion *oidc.JWTProfileAssertion, scopes oidc.Scopes, rp RelayingParty) (*oauth2.Token, error) {
|
|
||||||
token, err := GenerateJWTProfileToken(assertion)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return JWTProfileExchange(ctx, tokenexchange.NewJWTProfileRequest(token, scopes...), rp)
|
|
||||||
}
|
|
||||||
|
|
||||||
func GenerateJWTProfileToken(assertion *oidc.JWTProfileAssertion) (string, error) {
|
|
||||||
privateKey, err := bytesToPrivateKey(assertion.PrivateKey)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
key := jose.SigningKey{
|
|
||||||
Algorithm: jose.RS256,
|
|
||||||
Key: &jose.JSONWebKey{Key: privateKey, KeyID: assertion.PrivateKeyID},
|
|
||||||
}
|
|
||||||
signer, err := jose.NewSigner(key, &jose.SignerOptions{})
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
marshalledAssertion, err := json.Marshal(assertion)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
signedAssertion, err := signer.Sign(marshalledAssertion)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return signedAssertion.CompactSerialize()
|
|
||||||
}
|
|
||||||
|
|
||||||
func bytesToPrivateKey(priv []byte) (*rsa.PrivateKey, error) {
|
|
||||||
block, _ := pem.Decode(priv)
|
|
||||||
enc := x509.IsEncryptedPEMBlock(block)
|
|
||||||
b := block.Bytes
|
|
||||||
var err error
|
|
||||||
if enc {
|
|
||||||
b, err = x509.DecryptPEMBlock(block, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
key, err := x509.ParsePKCS1PrivateKey(b)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return key, nil
|
|
||||||
}
|
|
25
pkg/utils/key.go
Normal file
25
pkg/utils/key.go
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/pem"
|
||||||
|
)
|
||||||
|
|
||||||
|
func BytesToPrivateKey(priv []byte) (*rsa.PrivateKey, error) {
|
||||||
|
block, _ := pem.Decode(priv)
|
||||||
|
enc := x509.IsEncryptedPEMBlock(block)
|
||||||
|
b := block.Bytes
|
||||||
|
var err error
|
||||||
|
if enc {
|
||||||
|
b, err = x509.DecryptPEMBlock(block, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
key, err := x509.ParsePKCS1PrivateKey(b)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return key, nil
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue