diff --git a/cmd/config.go b/cmd/config.go index a37fec0..e89ce4a 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -11,30 +11,32 @@ import ( ) type Config struct { - Host string `yaml:"host"` - Port int `yaml:"port"` - RedirectUri []string `yaml:"redirect-uri"` - State string `yaml:"state"` - ResponseType string `yaml:"response-type"` - Scope []string `yaml:"scope"` - ClientId string `yaml:"client.id"` - ClientSecret string `yaml:"client.secret"` - OIDCHost string `yaml:"oidc.host"` - OIDCPort int `yaml:"oidc.port"` + Host string `yaml:"host"` + Port int `yaml:"port"` + RedirectUri []string `yaml:"redirect-uri"` + State string `yaml:"state"` + ResponseType string `yaml:"response-type"` + Scope []string `yaml:"scope"` + ClientId string `yaml:"client.id"` + ClientSecret string `yaml:"client.secret"` + OIDCHost string `yaml:"oidc.host"` + OIDCPort int `yaml:"oidc.port"` + IdentitiesUrl string `yaml:"identities-url"` } func NewConfig() Config { return Config{ - Host: "127.0.0.1", - Port: 3333, - RedirectUri: []string{""}, - State: util.RandomString(20), - ResponseType: "code", - Scope: []string{"openid", "profile", "email"}, - ClientId: "", - ClientSecret: "", - OIDCHost: "127.0.0.1", - OIDCPort: 80, + Host: "127.0.0.1", + Port: 3333, + RedirectUri: []string{""}, + State: util.RandomString(20), + ResponseType: "code", + Scope: []string{"openid", "profile", "email"}, + ClientId: "", + ClientSecret: "", + OIDCHost: "127.0.0.1", + OIDCPort: 80, + IdentitiesUrl: "", } } diff --git a/cmd/login.go b/cmd/login.go index 66d3d09..add207e 100644 --- a/cmd/login.go +++ b/cmd/login.go @@ -1,9 +1,10 @@ package cmd import ( + "davidallendj/oidc-auth/internal/api" "davidallendj/oidc-auth/internal/oidc" - "davidallendj/oidc-auth/internal/server" "davidallendj/oidc-auth/internal/util" + "encoding/json" "errors" "fmt" "net/http" @@ -12,6 +13,10 @@ import ( "github.com/spf13/cobra" ) +var ( + identitiesUrl = "" +) + var loginCmd = &cobra.Command{ Use: "login", Short: "Start the login flow", @@ -38,7 +43,7 @@ var loginCmd = &cobra.Command{ // authorize oauth client and listen for callback from provider fmt.Printf("Waiting for response from OIDC provider...\n") - code, err := server.WaitForAuthorizationCode(config.Host, config.Port) + code, err := api.WaitForAuthorizationCode(config.Host, config.Port) if errors.Is(err, http.ErrServerClosed) { fmt.Printf("server closed\n") } else if err != nil { @@ -47,12 +52,21 @@ var loginCmd = &cobra.Command{ } // use code from response and exchange for bearer token - server.FetchToken(code, oidcProvider.GetTokenUrl(), config.ClientId, config.ClientSecret, config.State, config.RedirectUri) + tokenString, err := api.FetchToken(code, oidcProvider.GetTokenUrl(), config.ClientId, config.ClientSecret, config.State, config.RedirectUri) + if err != nil { + fmt.Printf("%v\n", err) + return + } - // extract ID token and save user info - - // create a new identity with Ory Kratos + var data map[string]any + json.Unmarshal([]byte(tokenString), &data) + idToken := data["id_token"].(string) + // create a new identity with Ory Kratos if identitiesUrl is provided + if config.IdentitiesUrl != "" { + api.CreateIdentity(config.IdentitiesUrl, idToken) + api.FetchIdentities(config.IdentitiesUrl) + } // use ID token/user info to get access token from Ory Hydra }, } diff --git a/internal/api/api.go b/internal/api/api.go new file mode 100644 index 0000000..45a9560 --- /dev/null +++ b/internal/api/api.go @@ -0,0 +1,91 @@ +package api + +import ( + "bytes" + "fmt" + "io" + "net/http" + "net/url" + "strings" +) + +func WaitForAuthorizationCode(host string, port int) (string, error) { + var code string + s := &http.Server{ + Addr: fmt.Sprintf("%s:%d", host, port), + } + http.HandleFunc("/oauth/callback", func(w http.ResponseWriter, r *http.Request) { + // get the code from the OIDC provider + if r != nil { + code = r.URL.Query().Get("code") + fmt.Printf("Authorization Code: %v\n", code) + } + s.Close() + + }) + return code, s.ListenAndServe() +} + +func FetchToken(code string, remoteUrl string, clientId string, clientSecret string, state string, redirectUri []string) (string, error) { + var token string + data := url.Values{ + "grant_type": {"authorization_code"}, + "code": {code}, + "client_id": {clientId}, + "client_secret": {clientSecret}, + "state": {state}, + "redirect_uri": {strings.Join(redirectUri, ",")}, + } + res, err := http.PostForm(remoteUrl, data) + if err != nil { + return "", fmt.Errorf("failed to get token: %s", err) + } + defer res.Body.Close() + + b, err := io.ReadAll(res.Body) + if err != nil { + return "", fmt.Errorf("failed to read response body: %v", err) + } + token = string(b) + + fmt.Printf("%v\n", token) + return token, nil +} + +func CreateIdentity(remoteUrl string, idToken string) error { + data := []byte(`{ + "schema_id": "preset://email", + "traits": { + "email": "docs@example.org" + } + }`) + + req, err := http.NewRequest("POST", remoteUrl, bytes.NewBuffer(data)) + if err != nil { + return fmt.Errorf("failed to create a new request: %v", err) + } + req.Header.Add("Content-Type", "application/json") + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", idToken)) + client := &http.Client{} + res, err := client.Do(req) + if err != nil { + return fmt.Errorf("failed to do request: %v", err) + } + fmt.Printf("%d\n", res.StatusCode) + return nil +} + +func FetchIdentities(remoteUrl string) error { + req, err := http.NewRequest("GET", remoteUrl, bytes.NewBuffer([]byte{})) + if err != nil { + return fmt.Errorf("failed to create a new request: %v", err) + } + + client := &http.Client{} + res, err := client.Do(req) + if err != nil { + return fmt.Errorf("failed to do request: %v", err) + } + fmt.Printf("%v\n", res) + return nil +}