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:
Livio Amstutz 2021-02-15 13:43:50 +01:00 committed by GitHub
parent fa92a20615
commit 1518c843de
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
46 changed files with 1672 additions and 570 deletions

View file

@ -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
} }

View file

@ -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))

View file

@ -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 {

View 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
}

View file

@ -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
View 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
View 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
View 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
}

View 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)
}

View file

@ -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)

View file

@ -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

View file

@ -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)
}
}

View 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)
}

View file

@ -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)
}

View 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
}

View file

@ -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)

View file

@ -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"
)

View file

@ -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
View 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
View 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,
}
}

View file

@ -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
}

View file

@ -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 {

View file

@ -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 {

View file

@ -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
} }

View file

@ -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

View file

@ -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

View file

@ -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
} }

View file

@ -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,
}
}

View file

@ -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)
} }
}) })

View file

@ -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
} }

View file

@ -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()

View file

@ -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)
}

View file

@ -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
} }

View file

@ -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 {

View file

@ -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)
} }

View file

@ -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

View file

@ -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
} }

View 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")
}

View file

@ -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")

View file

@ -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 {

View file

@ -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
} }

View file

@ -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
View 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
}