Refactor previous COOPGO Identity service - Initial commit
This commit is contained in:
101
oidc-provider/endpoints_auth.go
Normal file
101
oidc-provider/endpoints_auth.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package op
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/csrf"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/ory/fosite"
|
||||
"github.com/ory/fosite/handler/openid"
|
||||
"github.com/ory/fosite/token/jwt"
|
||||
)
|
||||
|
||||
func (op *OIDCHandler) AuthEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
namespace := mux.Vars(r)["namespace"]
|
||||
oauth2Provider := op.NamespaceProviders[namespace]
|
||||
templates_dir := op.config.Namespaces[namespace].TemplatesDir
|
||||
|
||||
t := template.New("auth")
|
||||
t = template.Must(t.ParseFiles(
|
||||
templates_dir + "/auth.html",
|
||||
))
|
||||
|
||||
ctx := r.Context()
|
||||
ar, err := oauth2Provider.NewAuthorizeRequest(ctx, r)
|
||||
if err != nil {
|
||||
oauth2Provider.WriteAuthorizeError(w, ar, err)
|
||||
return
|
||||
}
|
||||
|
||||
if r.Method == "POST" {
|
||||
if r.Form.Get("username") == "" || r.Form.Get("password") == "" {
|
||||
oauth2Provider.WriteAuthorizeError(w, ar, fosite.ErrAccessDenied)
|
||||
return
|
||||
}
|
||||
|
||||
account, err := op.handler.Login(r.Form.Get("username"), r.Form.Get("password"), namespace)
|
||||
if err != nil {
|
||||
if err = t.ExecuteTemplate(w, "auth", map[string]any{
|
||||
csrf.TemplateTag: csrf.TemplateField(r),
|
||||
"error": fmt.Sprintf("Wrong username (%v) or password (%v) in namespace \"%v\"", r.Form.Get("username"), r.Form.Get("password"), namespace),
|
||||
"realError": err,
|
||||
}); err != nil {
|
||||
oauth2Provider.WriteAuthorizeError(w, ar, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
sessionData := &openid.DefaultSession{
|
||||
Claims: &jwt.IDTokenClaims{
|
||||
Issuer: fmt.Sprintf("http://%s/%s", r.Host, namespace),
|
||||
Subject: account.ID,
|
||||
Audience: []string{},
|
||||
ExpiresAt: time.Now().Add(time.Hour * 30),
|
||||
IssuedAt: time.Now(),
|
||||
RequestedAt: time.Now(),
|
||||
AuthTime: time.Now(),
|
||||
Extra: make(map[string]interface{}),
|
||||
},
|
||||
Username: r.Form.Get("username"),
|
||||
Subject: account.ID,
|
||||
Headers: &jwt.Headers{
|
||||
Extra: make(map[string]interface{}),
|
||||
},
|
||||
}
|
||||
|
||||
// Manage claims
|
||||
for _, v := range ar.GetRequestedScopes() {
|
||||
ar.GrantScope(v)
|
||||
|
||||
if v != "openid" { // TODO handle standard claims like profile, email, ...
|
||||
if mc, ok := op.config.Namespaces[namespace].MatchClaims[v]; ok {
|
||||
if d, ok := account.Data[mc]; ok {
|
||||
sessionData.Claims.Extra[v] = d
|
||||
}
|
||||
} else if d, ok := account.Data[v]; ok {
|
||||
sessionData.Claims.Extra[v] = d
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
response, err := oauth2Provider.NewAuthorizeResponse(ctx, ar, sessionData)
|
||||
if err != nil {
|
||||
oauth2Provider.WriteAuthorizeError(w, ar, err)
|
||||
return
|
||||
}
|
||||
|
||||
oauth2Provider.WriteAuthorizeResponse(w, ar, response)
|
||||
return
|
||||
}
|
||||
|
||||
err = t.ExecuteTemplate(w, "auth", map[string]any{
|
||||
// csrf.TemplateTag: csrf.TemplateField(r),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
26
oidc-provider/endpoints_introspection.go
Normal file
26
oidc-provider/endpoints_introspection.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package op
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/ory/fosite"
|
||||
)
|
||||
|
||||
func (op *OIDCHandler) IntrospectionEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
namespace := mux.Vars(r)["namespace"]
|
||||
oauth2Provider := op.NamespaceProviders[namespace]
|
||||
|
||||
ctx := r.Context()
|
||||
mySessionData := new(fosite.DefaultSession)
|
||||
ir, err := oauth2Provider.NewIntrospectionRequest(ctx, r, mySessionData)
|
||||
if err != nil {
|
||||
log.Printf("Error occurred in NewIntrospectionRequest: %+v", err)
|
||||
oauth2Provider.WriteIntrospectionError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
oauth2Provider.WriteIntrospectionResponse(w, ir)
|
||||
}
|
||||
41
oidc-provider/endpoints_token.go
Normal file
41
oidc-provider/endpoints_token.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package op
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/ory/fosite/handler/openid"
|
||||
)
|
||||
|
||||
func (op *OIDCHandler) TokenEndpoint(w http.ResponseWriter, req *http.Request) {
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
namespace := mux.Vars(req)["namespace"]
|
||||
provider := op.NamespaceProviders[namespace]
|
||||
|
||||
ctx := req.Context()
|
||||
|
||||
mySessionData := openid.NewDefaultSession()
|
||||
|
||||
accessRequest, err := provider.NewAccessRequest(ctx, req, mySessionData)
|
||||
if err != nil {
|
||||
fmt.Printf("Error occurred in NewAccessRequest: %+v", err)
|
||||
provider.WriteAccessError(w, accessRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
if accessRequest.GetGrantTypes().ExactOne("client_credentials") {
|
||||
for _, scope := range accessRequest.GetRequestedScopes() {
|
||||
accessRequest.GrantScope(scope)
|
||||
}
|
||||
}
|
||||
|
||||
response, err := provider.NewAccessResponse(ctx, accessRequest)
|
||||
if err != nil {
|
||||
fmt.Printf("Error occurred in NewAccessResponse: %+v", err)
|
||||
provider.WriteAccessError(w, accessRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
provider.WriteAccessResponse(w, accessRequest, response)
|
||||
}
|
||||
8
oidc-provider/endpoints_userinfo.go
Normal file
8
oidc-provider/endpoints_userinfo.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package op
|
||||
|
||||
import "net/http"
|
||||
|
||||
func (op *OIDCHandler) UserinfoEndpoint(w http.ResponseWriter, req *http.Request) {
|
||||
// TODO
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}
|
||||
62
oidc-provider/endpoints_wellknown.go
Normal file
62
oidc-provider/endpoints_wellknown.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package op
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"gopkg.in/square/go-jose.v2"
|
||||
)
|
||||
|
||||
func (op *OIDCHandler) WellKnownOIDCEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
var (
|
||||
host = r.Host
|
||||
namespace = mux.Vars(r)["namespace"]
|
||||
issuer = fmt.Sprintf("http://%s/%s", host, namespace)
|
||||
)
|
||||
|
||||
response := map[string]any{
|
||||
"issuer": issuer,
|
||||
"authorization_endpoint": issuer + "/auth",
|
||||
"token_endpoint": issuer + "/token",
|
||||
"userinfo_endpoint": issuer + "/userinfo",
|
||||
"id_token_signing_alg_values_supported": []string{"RS256"},
|
||||
"grant_types_supported": []string{"authorization_code", "implicit", "client_credentials", "refresh_token"},
|
||||
"response_types": []string{"code", "code id_token", "id_token", "token id_token", "token", "token id_token code"},
|
||||
"response_modes_supported": []string{"query", "fragment"},
|
||||
"jwks_uri": issuer + "/.well-known/jwks.json",
|
||||
}
|
||||
|
||||
json, err := json.Marshal(response)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write(json)
|
||||
}
|
||||
|
||||
func (op *OIDCHandler) WellKnownJWKSEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
jwks := &jose.JSONWebKeySet{
|
||||
Keys: []jose.JSONWebKey{
|
||||
{
|
||||
KeyID: "kid-foo",
|
||||
Use: "sig",
|
||||
Key: &op.PrivateKey.PublicKey,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
jsonJwks, err := json.Marshal(jwks)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write(jsonJwks)
|
||||
}
|
||||
565
oidc-provider/fosite.go
Normal file
565
oidc-provider/fosite.go
Normal file
@@ -0,0 +1,565 @@
|
||||
package op
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rsa"
|
||||
"errors"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
"git.coopgo.io/coopgo-platform/mobility-accounts/handlers"
|
||||
"git.coopgo.io/coopgo-platform/mobility-accounts/storage"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/ory/fosite"
|
||||
"github.com/ory/fosite/compose"
|
||||
"github.com/ory/fosite/handler/openid"
|
||||
"gopkg.in/square/go-jose.v2"
|
||||
)
|
||||
|
||||
func NewProvider(c OIDCNamespaceConfig, h handlers.MobilityAccountsHandler, s storage.Storage, privateKey *rsa.PrivateKey) fosite.OAuth2Provider {
|
||||
|
||||
config := &compose.Config{}
|
||||
storage := NewOIDCProviderStore(c, h, s.KV)
|
||||
secret := []byte(c.SecretKey)
|
||||
return compose.ComposeAllEnabled(config, storage, secret, privateKey)
|
||||
}
|
||||
|
||||
type OIDCProviderStore struct {
|
||||
Namespace string
|
||||
MobilityAccountsHandler handlers.MobilityAccountsHandler
|
||||
KV storage.KVStore
|
||||
Clients map[string]fosite.Client
|
||||
}
|
||||
|
||||
func NewOIDCProviderStore(c OIDCNamespaceConfig, h handlers.MobilityAccountsHandler, storage storage.KVStore) *OIDCProviderStore {
|
||||
clients := map[string]fosite.Client{}
|
||||
|
||||
for _, v := range c.Clients {
|
||||
client := &fosite.DefaultClient{
|
||||
ID: v.ID,
|
||||
Secret: []byte(v.Secret),
|
||||
RedirectURIs: v.RedirectURIs,
|
||||
ResponseTypes: v.ResponseTypes,
|
||||
GrantTypes: v.GrantTypes,
|
||||
Scopes: v.Scopes,
|
||||
Audience: v.Audience,
|
||||
Public: v.Public,
|
||||
}
|
||||
|
||||
if v.OIDC {
|
||||
oidc_client := &fosite.DefaultOpenIDConnectClient{
|
||||
DefaultClient: client,
|
||||
TokenEndpointAuthMethod: v.TokenEndpointAuthMethod,
|
||||
}
|
||||
clients[v.ID] = oidc_client
|
||||
} else {
|
||||
clients[v.ID] = client
|
||||
}
|
||||
}
|
||||
|
||||
return &OIDCProviderStore{
|
||||
MobilityAccountsHandler: h,
|
||||
KV: storage,
|
||||
Clients: clients,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *OIDCProviderStore) GetClient(_ context.Context, id string) (fosite.Client, error) {
|
||||
cl, ok := s.Clients[id]
|
||||
if !ok {
|
||||
return nil, fosite.ErrNotFound
|
||||
}
|
||||
return cl, nil
|
||||
}
|
||||
|
||||
func (s *OIDCProviderStore) Authenticate(_ context.Context, name string, secret string) error {
|
||||
_, err := s.MobilityAccountsHandler.Login(name, secret, s.Namespace)
|
||||
if err != nil {
|
||||
return fosite.ErrNotFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *OIDCProviderStore) CreateOpenIDConnectSession(_ context.Context, authorizeCode string, requester fosite.Requester) error {
|
||||
err := s.KV.Put("id_sessions/"+authorizeCode, requester)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *OIDCProviderStore) GetOpenIDConnectSession(_ context.Context, authorizeCode string, requester fosite.Requester) (fosite.Requester, error) {
|
||||
d, err := s.KV.Get("id_sessions/" + authorizeCode)
|
||||
if err != nil {
|
||||
return nil, fosite.ErrNotFound
|
||||
}
|
||||
//return d.(fosite.Requester), nil
|
||||
|
||||
return DecodeRequest(d)
|
||||
|
||||
// req := fosite.NewRequest()
|
||||
// req.Session = new(openid.DefaultSession)
|
||||
|
||||
// decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
|
||||
// Metadata: nil,
|
||||
// DecodeHook: mapstructure.ComposeDecodeHookFunc(
|
||||
// ToTimeHookFunc()),
|
||||
// Result: &req,
|
||||
// })
|
||||
// if err != nil {
|
||||
// return req, err
|
||||
// }
|
||||
// if err = decoder.Decode(d); err != nil {
|
||||
// return req, err
|
||||
// }
|
||||
|
||||
// return req, nil
|
||||
}
|
||||
|
||||
// DeleteOpenIDConnectSession is not really called from anywhere and it is deprecated.
|
||||
func (s *OIDCProviderStore) DeleteOpenIDConnectSession(_ context.Context, authorizeCode string) error {
|
||||
err := s.KV.Delete("id_sessions/" + authorizeCode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *OIDCProviderStore) ClientAssertionJWTValid(_ context.Context, jti string) error {
|
||||
if _, exists := s.KV.Get("blacklisted_jtis/" + jti); exists == nil {
|
||||
return fosite.ErrJTIKnown
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *OIDCProviderStore) SetClientAssertionJWT(_ context.Context, jti string, exp time.Time) error {
|
||||
|
||||
if _, exists := s.KV.Get("blacklisted_jtis/" + jti); exists == nil {
|
||||
return fosite.ErrJTIKnown
|
||||
}
|
||||
|
||||
duration := exp.Sub(time.Now())
|
||||
|
||||
if duration < 0 {
|
||||
return errors.New("already expired")
|
||||
}
|
||||
|
||||
err := s.KV.PutWithTTL("blacklisted_jtis/"+jti, true, duration)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *OIDCProviderStore) CreateAuthorizeCodeSession(_ context.Context, code string, req fosite.Requester) error {
|
||||
res := StoreAuthorizeCode{
|
||||
Active: true,
|
||||
Requester: req.(*fosite.Request),
|
||||
}
|
||||
err := s.KV.Put("authorize_codes/"+code, res)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *OIDCProviderStore) GetAuthorizeCodeSession(_ context.Context, code string, _ fosite.Session) (fosite.Requester, error) {
|
||||
rel, err := s.KV.Get("authorize_codes/" + code)
|
||||
if err != nil {
|
||||
return nil, fosite.ErrNotFound
|
||||
}
|
||||
|
||||
sac, err := DecodeStoreAuthorizeCode(rel)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !sac.Active {
|
||||
return sac.Requester, fosite.ErrInvalidatedAuthorizeCode
|
||||
}
|
||||
|
||||
return sac.Requester, nil
|
||||
}
|
||||
|
||||
func (s *OIDCProviderStore) InvalidateAuthorizeCodeSession(ctx context.Context, code string) error {
|
||||
rel, err := s.KV.Get("authorize_codes/" + code)
|
||||
if err != nil {
|
||||
return fosite.ErrNotFound
|
||||
}
|
||||
|
||||
sac, err := DecodeStoreAuthorizeCode(rel)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sac.Active = false
|
||||
err = s.KV.Put("authorize_codes/"+code, sac)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *OIDCProviderStore) CreatePKCERequestSession(_ context.Context, code string, req fosite.Requester) error {
|
||||
err := s.KV.Put("pkce/"+code, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *OIDCProviderStore) GetPKCERequestSession(_ context.Context, code string, _ fosite.Session) (fosite.Requester, error) {
|
||||
rel, err := s.KV.Get("pkce/" + code)
|
||||
if err != nil {
|
||||
return nil, fosite.ErrNotFound
|
||||
}
|
||||
|
||||
req := fosite.NewRequest()
|
||||
req.Session = new(fosite.DefaultSession)
|
||||
|
||||
decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
|
||||
Metadata: nil,
|
||||
DecodeHook: mapstructure.ComposeDecodeHookFunc(
|
||||
ToTimeHookFunc()),
|
||||
Result: &req,
|
||||
})
|
||||
if err != nil {
|
||||
return req, err
|
||||
}
|
||||
if err = decoder.Decode(rel); err != nil {
|
||||
return req, err
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func (s *OIDCProviderStore) DeletePKCERequestSession(_ context.Context, code string) error {
|
||||
err := s.KV.Delete("pkce/" + code)
|
||||
if err != nil {
|
||||
return fosite.ErrNotFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *OIDCProviderStore) CreateAccessTokenSession(_ context.Context, signature string, req fosite.Requester) error {
|
||||
err := s.KV.Put("access_tokens/"+signature, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = s.KV.Put("access_tokens_request_ids/"+req.GetID(), signature)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *OIDCProviderStore) GetAccessTokenSession(_ context.Context, signature string, _ fosite.Session) (fosite.Requester, error) {
|
||||
rel, err := s.KV.Get("access_tokens/" + signature)
|
||||
if err != nil {
|
||||
return nil, fosite.ErrNotFound
|
||||
}
|
||||
//return rel.(fosite.Requester), nil
|
||||
req := fosite.NewRequest()
|
||||
req.Session = new(fosite.DefaultSession)
|
||||
|
||||
decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
|
||||
Metadata: nil,
|
||||
DecodeHook: mapstructure.ComposeDecodeHookFunc(
|
||||
ToTimeHookFunc()),
|
||||
Result: &req,
|
||||
})
|
||||
if err != nil {
|
||||
return req, err
|
||||
}
|
||||
if err = decoder.Decode(rel); err != nil {
|
||||
return req, err
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func (s *OIDCProviderStore) DeleteAccessTokenSession(_ context.Context, signature string) error {
|
||||
err := s.KV.Delete("access_tokens/" + signature)
|
||||
if err != nil {
|
||||
return fosite.ErrNotFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *OIDCProviderStore) CreateRefreshTokenSession(_ context.Context, signature string, req fosite.Requester) error {
|
||||
|
||||
err := s.KV.Put("refresh_tokens/"+signature, StoreRefreshToken{Active: true, Requester: req})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = s.KV.Put("refresh_tokens_request_ids/"+req.GetID(), signature)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *OIDCProviderStore) GetRefreshTokenSession(_ context.Context, signature string, _ fosite.Session) (fosite.Requester, error) {
|
||||
rel, err := s.KV.Get("refresh_tokens/" + signature)
|
||||
if err != nil {
|
||||
return nil, fosite.ErrNotFound
|
||||
}
|
||||
var srt StoreRefreshToken
|
||||
if err = mapstructure.Decode(rel.(map[string]any), &srt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !srt.Active {
|
||||
return nil, fosite.ErrInactiveToken
|
||||
}
|
||||
return srt.Requester, nil
|
||||
}
|
||||
|
||||
func (s *OIDCProviderStore) DeleteRefreshTokenSession(_ context.Context, signature string) error {
|
||||
err := s.KV.Delete("refresh_tokens/" + signature)
|
||||
if err != nil {
|
||||
return fosite.ErrNotFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *OIDCProviderStore) RevokeRefreshToken(ctx context.Context, requestID string) error {
|
||||
|
||||
if signature, err := s.KV.Get("refresh_tokens_request_ids" + requestID); err == nil {
|
||||
rel, err := s.KV.Get("refresh_tokens/" + signature.(string))
|
||||
if err != nil {
|
||||
return fosite.ErrNotFound
|
||||
}
|
||||
var srt StoreRefreshToken
|
||||
if err = mapstructure.Decode(rel.(map[string]any), &srt); err != nil {
|
||||
return err
|
||||
}
|
||||
srt.Active = false
|
||||
|
||||
err = s.KV.Put("refresh_tokens/"+signature.(string), srt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *OIDCProviderStore) RevokeRefreshTokenMaybeGracePeriod(ctx context.Context, requestID string, signature string) error {
|
||||
// no configuration option is available; grace period is not available with memory store
|
||||
return s.RevokeRefreshToken(ctx, requestID)
|
||||
}
|
||||
|
||||
func (s *OIDCProviderStore) RevokeAccessToken(ctx context.Context, requestID string) error {
|
||||
if signature, err := s.KV.Get("access_tokens_request_ids/" + requestID); err != nil {
|
||||
if err := s.DeleteAccessTokenSession(ctx, signature.(string)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *OIDCProviderStore) GetPublicKey(ctx context.Context, issuer string, subject string, keyId string) (*jose.JSONWebKey, error) {
|
||||
if issuerKeys, err := s.KV.Get("issuer_public_keys/" + issuer); err == nil {
|
||||
var ipk IssuerPublicKeys
|
||||
if err = mapstructure.Decode(issuerKeys.(map[string]any), &ipk); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if subKeys, ok := ipk.KeysBySub[subject]; ok {
|
||||
if keyScopes, ok := subKeys.Keys[keyId]; ok {
|
||||
return keyScopes.Key, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fosite.ErrNotFound
|
||||
}
|
||||
func (s *OIDCProviderStore) GetPublicKeys(ctx context.Context, issuer string, subject string) (*jose.JSONWebKeySet, error) {
|
||||
|
||||
if issuerKeys, err := s.KV.Get("issuer_public_keys/" + issuer); err == nil {
|
||||
var ipk IssuerPublicKeys
|
||||
if err = mapstructure.Decode(issuerKeys.(map[string]any), &ipk); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if subKeys, ok := ipk.KeysBySub[subject]; ok {
|
||||
if len(subKeys.Keys) == 0 {
|
||||
return nil, fosite.ErrNotFound
|
||||
}
|
||||
|
||||
keys := make([]jose.JSONWebKey, 0, len(subKeys.Keys))
|
||||
for _, keyScopes := range subKeys.Keys {
|
||||
keys = append(keys, *keyScopes.Key)
|
||||
}
|
||||
|
||||
return &jose.JSONWebKeySet{Keys: keys}, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fosite.ErrNotFound
|
||||
}
|
||||
|
||||
func (s *OIDCProviderStore) GetPublicKeyScopes(ctx context.Context, issuer string, subject string, keyId string) ([]string, error) {
|
||||
|
||||
if issuerKeys, err := s.KV.Get("issuer_public_keys/" + issuer); err == nil {
|
||||
var ipk IssuerPublicKeys
|
||||
if err = mapstructure.Decode(issuerKeys.(map[string]any), &ipk); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if subKeys, ok := ipk.KeysBySub[subject]; ok {
|
||||
if keyScopes, ok := subKeys.Keys[keyId]; ok {
|
||||
return keyScopes.Scopes, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fosite.ErrNotFound
|
||||
}
|
||||
|
||||
func (s *OIDCProviderStore) IsJWTUsed(ctx context.Context, jti string) (bool, error) {
|
||||
err := s.ClientAssertionJWTValid(ctx, jti)
|
||||
if err != nil {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (s *OIDCProviderStore) MarkJWTUsedForTime(ctx context.Context, jti string, exp time.Time) error {
|
||||
return s.SetClientAssertionJWT(ctx, jti, exp)
|
||||
}
|
||||
|
||||
// CreatePARSession stores the pushed authorization request context. The requestURI is used to derive the key.
|
||||
func (s *OIDCProviderStore) CreatePARSession(ctx context.Context, requestURI string, request fosite.AuthorizeRequester) error {
|
||||
err := s.KV.Put("par_sessions/"+requestURI, request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetPARSession gets the push authorization request context. If the request is nil, a new request object
|
||||
// is created. Otherwise, the same object is updated.
|
||||
func (s *OIDCProviderStore) GetPARSession(ctx context.Context, requestURI string) (fosite.AuthorizeRequester, error) {
|
||||
rel, err := s.KV.Get("par_sessions/" + requestURI)
|
||||
if err != nil {
|
||||
return nil, fosite.ErrNotFound
|
||||
}
|
||||
|
||||
return rel.(fosite.AuthorizeRequester), nil
|
||||
}
|
||||
|
||||
// DeletePARSession deletes the context.
|
||||
func (s *OIDCProviderStore) DeletePARSession(ctx context.Context, requestURI string) error {
|
||||
err := s.KV.Delete("par_sessions/" + requestURI)
|
||||
if err != nil {
|
||||
return fosite.ErrNotFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type StoreAuthorizeCode struct {
|
||||
Active bool `json:"active"`
|
||||
Requester *fosite.Request `json:"requester"`
|
||||
}
|
||||
|
||||
func DecodeStoreAuthorizeCode(rel interface{}) (StoreAuthorizeCode, error) {
|
||||
sac := StoreAuthorizeCode{
|
||||
Active: false,
|
||||
Requester: fosite.NewRequest(),
|
||||
}
|
||||
// metadata := mapstructure.Metadata{}
|
||||
sac.Requester.Session = new(openid.DefaultSession)
|
||||
decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
|
||||
// Metadata: &metadata,
|
||||
DecodeHook: mapstructure.ComposeDecodeHookFunc(
|
||||
ToTimeHookFunc(),
|
||||
ToBytes(),
|
||||
),
|
||||
TagName: "json",
|
||||
Result: &sac,
|
||||
})
|
||||
if err != nil {
|
||||
return sac, err
|
||||
}
|
||||
if err = decoder.Decode(rel); err != nil {
|
||||
return sac, err
|
||||
}
|
||||
return sac, nil
|
||||
}
|
||||
|
||||
func DecodeRequest(rel interface{}) (*fosite.Request, error) {
|
||||
req := fosite.NewRequest()
|
||||
req.Session = new(openid.DefaultSession)
|
||||
decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
|
||||
// Metadata: &metadata,
|
||||
DecodeHook: mapstructure.ComposeDecodeHookFunc(
|
||||
ToTimeHookFunc(),
|
||||
ToBytes(),
|
||||
),
|
||||
TagName: "json",
|
||||
Result: &req,
|
||||
})
|
||||
if err != nil {
|
||||
return req, err
|
||||
}
|
||||
if err = decoder.Decode(rel); err != nil {
|
||||
return req, err
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
|
||||
type StoreRefreshToken struct {
|
||||
Active bool
|
||||
Requester fosite.Requester
|
||||
}
|
||||
|
||||
type IssuerPublicKeys struct {
|
||||
Issuer string
|
||||
KeysBySub map[string]SubjectPublicKeys `mapstructure:"keys_by_sub"`
|
||||
}
|
||||
|
||||
type SubjectPublicKeys struct {
|
||||
Subject string
|
||||
Keys map[string]PublicKeyScopes
|
||||
}
|
||||
|
||||
type PublicKeyScopes struct {
|
||||
Key *jose.JSONWebKey
|
||||
Scopes []string
|
||||
}
|
||||
|
||||
func ToTimeHookFunc() mapstructure.DecodeHookFunc {
|
||||
return func(
|
||||
f reflect.Type,
|
||||
t reflect.Type,
|
||||
data interface{}) (interface{}, error) {
|
||||
if t != reflect.TypeOf(time.Time{}) {
|
||||
return data, nil
|
||||
}
|
||||
|
||||
switch f.Kind() {
|
||||
case reflect.String:
|
||||
return time.Parse(time.RFC3339, data.(string))
|
||||
case reflect.Float64:
|
||||
return time.Unix(0, int64(data.(float64))*int64(time.Millisecond)), nil
|
||||
case reflect.Int64:
|
||||
return time.Unix(0, data.(int64)*int64(time.Millisecond)), nil
|
||||
default:
|
||||
return data, nil
|
||||
}
|
||||
// Convert it by parsing
|
||||
}
|
||||
}
|
||||
|
||||
func ToBytes() mapstructure.DecodeHookFunc {
|
||||
return func(
|
||||
f reflect.Type,
|
||||
t reflect.Type,
|
||||
data interface{}) (interface{}, error) {
|
||||
|
||||
if t == reflect.TypeOf([]byte("")) && f.Kind() == reflect.String {
|
||||
return []byte(data.(string)), nil
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
}
|
||||
93
oidc-provider/oidc-provider.go
Normal file
93
oidc-provider/oidc-provider.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package op
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"fmt"
|
||||
|
||||
"git.coopgo.io/coopgo-platform/mobility-accounts/handlers"
|
||||
"git.coopgo.io/coopgo-platform/mobility-accounts/storage"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/ory/fosite"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type OIDCConfig struct {
|
||||
Enable bool
|
||||
CSRFKey bool `mapstructure:"csrf_key"`
|
||||
Port bool
|
||||
Namespaces map[string]OIDCNamespaceConfig
|
||||
}
|
||||
|
||||
type OIDCNamespaceConfig struct {
|
||||
Namespace string
|
||||
SecretKey string `mapstructure:"secret_key"`
|
||||
TemplatesDir string `mapstructure:"templates_dir"`
|
||||
MatchClaims map[string]string `mapstructure:"match_claims"`
|
||||
Clients []OIDCClient
|
||||
}
|
||||
|
||||
type OIDCClient struct {
|
||||
ID string
|
||||
OIDC bool
|
||||
Secret string
|
||||
RedirectURIs []string `mapstructure:"redirect_uris"`
|
||||
ResponseTypes []string `mapstructure:"response_types"`
|
||||
GrantTypes []string `mapstructure:"grant_types"`
|
||||
Scopes []string
|
||||
Audience []string
|
||||
Public bool
|
||||
//OIDC specific
|
||||
TokenEndpointAuthMethod string `mapstructure:"token_endpoint_auth_method"`
|
||||
}
|
||||
|
||||
type OIDCHandler struct {
|
||||
NamespaceProviders map[string]fosite.OAuth2Provider
|
||||
config OIDCConfig
|
||||
handler handlers.MobilityAccountsHandler
|
||||
PrivateKey *rsa.PrivateKey
|
||||
}
|
||||
|
||||
func NewOIDCHandler(h handlers.MobilityAccountsHandler, storage storage.Storage, config *viper.Viper) *OIDCHandler {
|
||||
var oidc_config OIDCConfig
|
||||
|
||||
mapstructure.Decode(config.Get("services.oidc_provider").(map[string]any), &oidc_config)
|
||||
|
||||
providers := map[string]fosite.OAuth2Provider{}
|
||||
|
||||
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, c := range oidc_config.Namespaces {
|
||||
np := NewProvider(c, h, storage, privateKey)
|
||||
|
||||
providers[c.Namespace] = np
|
||||
}
|
||||
|
||||
return &OIDCHandler{
|
||||
config: oidc_config,
|
||||
handler: h,
|
||||
NamespaceProviders: providers,
|
||||
PrivateKey: privateKey,
|
||||
}
|
||||
}
|
||||
|
||||
func Run(done chan error, cfg *viper.Viper, handler handlers.MobilityAccountsHandler, storage storage.Storage) {
|
||||
var (
|
||||
address = "0.0.0.0:" + cfg.GetString("services.oidc_provider.port")
|
||||
)
|
||||
|
||||
fmt.Println("-> OIDC provider endpoints on", address)
|
||||
|
||||
s := NewOIDCHandler(handler, storage, cfg)
|
||||
|
||||
err := NewOIDCServer(s, cfg)
|
||||
|
||||
if err != nil {
|
||||
fmt.Println("OIDC server ended")
|
||||
}
|
||||
|
||||
done <- err
|
||||
}
|
||||
40
oidc-provider/server.go
Normal file
40
oidc-provider/server.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package op
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/csrf"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func NewOIDCServer(oidc_handler *OIDCHandler, cfg *viper.Viper) error {
|
||||
var (
|
||||
dev_env = cfg.GetBool("dev_env")
|
||||
address = "0.0.0.0:" + cfg.GetString("services.oidc_provider.port")
|
||||
//csrf_key = cfg.GetString("services.oidc_provider.csrf_key")
|
||||
)
|
||||
|
||||
router := mux.NewRouter()
|
||||
router.HandleFunc("/{namespace}/auth", oidc_handler.AuthEndpoint)
|
||||
router.HandleFunc("/{namespace}/token", oidc_handler.TokenEndpoint)
|
||||
router.HandleFunc("/{namespace}/introspect", oidc_handler.IntrospectionEndpoint)
|
||||
router.HandleFunc("/{namespace}/userinfo", oidc_handler.UserinfoEndpoint)
|
||||
router.HandleFunc("/{namespace}/.well-known/openid-configuration", oidc_handler.WellKnownOIDCEndpoint)
|
||||
router.HandleFunc("/{namespace}/.well-known/jwks.json", oidc_handler.WellKnownJWKSEndpoint)
|
||||
|
||||
if dev_env {
|
||||
csrf.Secure(false)
|
||||
}
|
||||
|
||||
srv := &http.Server{
|
||||
Handler: router,
|
||||
Addr: address,
|
||||
WriteTimeout: 15 * time.Second,
|
||||
ReadTimeout: 15 * time.Second,
|
||||
}
|
||||
err := srv.ListenAndServe()
|
||||
|
||||
return err
|
||||
}
|
||||
16
oidc-provider/templates/default/auth.html
Normal file
16
oidc-provider/templates/default/auth.html
Normal file
@@ -0,0 +1,16 @@
|
||||
{{define "auth"}}
|
||||
<html>
|
||||
<head></head>
|
||||
<body>
|
||||
<h1>Login page</h1>
|
||||
<p>{{.error}}</p>
|
||||
<p>{{.realError}}</p>
|
||||
<form method="post">
|
||||
{{ .csrfField }}
|
||||
Username : <input type="text" name="username" />
|
||||
Password : <input type="text" name="password" />
|
||||
<input type="submit" value="Submit">
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
{{end}}
|
||||
Reference in New Issue
Block a user