Timmy Welch a9a40622ca
Add compatibility with Zitadel
Expand ~ to HOME in Agent.Socket
Add url override for gcloud functions v2
Add logging for parsing the principals
go fmt
2023-01-14 10:49:39 -08:00

128 lines
3.7 KiB
Go

package sshrimpagent
import (
"fmt"
"net"
"net/http"
"net/url"
"time"
"git.narnian.us/lordwelch/sshrimp/internal/config"
"github.com/google/uuid"
"github.com/zitadel/oidc/pkg/client/rp"
httphelper "github.com/zitadel/oidc/pkg/http"
"github.com/zitadel/oidc/pkg/oidc"
"golang.org/x/exp/slices"
)
var (
key = []byte(uuid.New().String())[:16]
)
type OidcClient struct {
ListenAddress string
*http.Server
oidcMux *http.ServeMux
OIDCToken chan *oidc.Tokens
*config.SSHrimp
}
func newOIDCClient(c *config.SSHrimp) (OidcClient, error) {
if len(c.Agent.Scopes) < 1 {
c.Agent.Scopes = []string{"openid", "email", "profile"}
}
if !slices.Contains(c.Agent.Scopes, "openid") {
c.Agent.Scopes = append([]string{"scopes"}, c.Agent.Scopes...)
}
token_chan := make(chan *oidc.Tokens)
oidcMux := http.NewServeMux()
return OidcClient{
oidcMux: oidcMux,
Server: &http.Server{
Addr: fmt.Sprintf("localhost:%d", c.Agent.Port),
Handler: oidcMux,
ReadTimeout: time.Minute / 2,
ReadHeaderTimeout: time.Minute / 2,
WriteTimeout: time.Minute / 2,
IdleTimeout: time.Minute / 2,
},
OIDCToken: token_chan,
SSHrimp: c,
}, nil
}
func (o *OidcClient) baseURI() url.URL {
return url.URL{
Scheme: "http",
Host: o.Addr,
}
}
func (o *OidcClient) ListenAndServe() error {
ln, err := net.Listen("tcp", o.Addr)
if err != nil {
return err
}
o.Addr = ln.Addr().String()
if err = o.setupHandlers(); err != nil {
return err
}
return o.Server.Serve(ln)
}
func (o *OidcClient) setupHandlers() error {
redirectURI := o.baseURI()
redirectURI.Path = "/auth/callback"
successURI := o.baseURI()
successURI.RawQuery = url.Values{"auth": []string{"success"}}.Encode()
// failURI := o.baseURI()
// failURI.RawQuery = url.Values{"auth":[]string{"fail"}}.Encode()
cookieHandler := httphelper.NewCookieHandler(key, key, httphelper.WithUnsecure())
options := []rp.Option{
rp.WithCookieHandler(cookieHandler),
rp.WithVerifierOpts(rp.WithIssuedAtOffset(5 * time.Second)),
}
if o.Agent.ClientSecret == "" {
options = append(options, rp.WithPKCE(cookieHandler))
}
if o.Agent.KeyPath != "" {
options = append(options, rp.WithJWTProfile(rp.SignerFromKeyPath(o.Agent.KeyPath)))
}
provider, err := rp.NewRelyingPartyOIDC(o.Agent.ProviderURL, o.Agent.ClientID, o.Agent.ClientSecret, redirectURI.String(), o.Agent.Scopes, options...)
if err != nil {
return fmt.Errorf("error creating provider: %w", err)
}
// generate some state (representing the state of the user in your application,
// e.g. the page where he was before sending him to login
state := func() string {
return uuid.New().String()
}
// register the AuthURLHandler at your preferred path
// the AuthURLHandler creates the auth request and redirects the user to the auth server
// including state handling with secure cookie and the possibility to use PKCE
o.oidcMux.Handle("/login", rp.AuthURLHandler(state, provider))
// o.oidcMux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// fmt.Fprintln(w, "Return to the CLI.")
// }))
// for demonstration purposes the returned userinfo response is written as JSON object onto response
marshalUserinfo := func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens, state string, rp rp.RelyingParty) {
o.OIDCToken <- tokens
w.Header().Add("location", successURI.String())
w.WriteHeader(301)
}
// register the CodeExchangeHandler at the callbackPath
// the CodeExchangeHandler handles the auth response, creates the token request and calls the callback function
// with the returned tokens from the token endpoint
o.oidcMux.Handle(redirectURI.Path, rp.CodeExchangeHandler(marshalUserinfo, provider))
return nil
}