From 4bca62ec2fb2cfc80a84d3588004163b000d6e03 Mon Sep 17 00:00:00 2001 From: "David J. Allen" Date: Sun, 3 Mar 2024 18:23:35 -0700 Subject: [PATCH] Refactor and added ability to use include multiple providers in config --- config.yaml | 98 +++++--- internal/authenticate.go | 118 +++++++++ internal/authorize.go | 241 +++++++++++++++++++ internal/client.go | 342 +++++---------------------- internal/config.go | 99 +++++--- internal/flows/authorization_code.go | 254 -------------------- internal/flows/client_credentials.go | 29 --- internal/flows/login.go | 28 --- internal/identities.go | 32 +++ internal/login.go | 37 +++ internal/oidc/oidc.go | 57 ++--- internal/opaal.go | 17 -- internal/server.go | 20 +- 13 files changed, 660 insertions(+), 712 deletions(-) create mode 100644 internal/authenticate.go create mode 100644 internal/authorize.go delete mode 100644 internal/flows/authorization_code.go delete mode 100644 internal/flows/client_credentials.go delete mode 100644 internal/flows/login.go create mode 100644 internal/identities.go create mode 100644 internal/login.go delete mode 100644 internal/opaal.go diff --git a/config.yaml b/config.yaml index c2530ec..02c1c0a 100755 --- a/config.yaml +++ b/config.yaml @@ -1,29 +1,73 @@ +version: "0.0.1" server: - host: 127.0.0.1 + host: "127.0.0.1" port: 3333 -client: - id: 7527e7b4-c96a-4df0-8fc5-00fde18bb65d - secret: gto_cc5uvpb5lsdczkwnbarvwmbpv5kcjwg7nhbc75zt65yrfh2ldenq - redirect-uris: - - "http://127.0.0.1:3333/oidc/callback" -oidc: - issuer: "http://git.towk.local:3000/" -urls: - #identities: http://127.0.0.1:4434/admin/identities - trusted-issuers: http://127.0.0.1:4445/admin/trust/grants/jwt-bearer/issuers - access-token: http://127.0.0.1:4444/oauth2/token - server-config: http://git.towk.local:3000/.well-known/openid-configuration - jwks_uri: http://git.towk.local:3000/login/oauth/keys - login: http://127.0.0.1:4433/self-service/login/api - login-flow-id: http://127.0.0.1:4433/self-service/login/flows?id={id} - register-client: http://127.0.0.1:4445/clients - authorize-client: http://127.0.0.1:4444/oauth2/authorize -state: "" -response-type: code -decode-id-token: true -decode-access-token: true -run-once: true -scope: -- openid -- profile -- email + callback: "/oidc/callback" + +providers: + facebook: "http://facebook.com" + forgejo: "http://git.towk.local:3000" + gitlab: "https://gitlab.newmexicoconsortium.org" + github: "https://github.com" + +authentication: + clients: + - id: "1135541217802147" + secret: "b3a3123e8235de1dbab448369bc3d024" + issuer: "https://www.facebook.com" + scope: + - "openid" + - "name" + - "email" + redirect-uris: + - "http://127.0.0.1:3333/oidc/callback" + - id: "978b48059dd4916f53b4" + secret: "eb54b533eb6afd695e3a1b3f363ab2b29acc7425" + issuer: "https://github.com" + scope: + - "openid" + - "profile" + redirect-uris: + - "http://127.0.0.1:3333/oidc/callback" + - id: "7527e7b4-c96a-4df0-8fc5-00fde18bb65d" + secret: "gto_cc5uvpb5lsdczkwnbarvwmbpv5kcjwg7nhbc75zt65yrfh2ldenq" + name: "forgejo" + issuer: "http://git.towk.local:3000" + scope: + - "openid" + - "profile" + - "read" + - "email" + redirect-uris: + - "http://127.0.0.1:3333/oidc/callback" + - id: "7c0fab1153674a258a705976fcb9468350df3addd91de4ec622fc9ed24bfbcdd" + secret: "a9a8bc55b0cd99236756093adc00ab17855fa507ce106b8038e7f9390ef2ad99" + name: "gitlab" + issuer: "http://gitlab.newmexicoconsortium.org" + scope: + - "openid" + - "profile" + - "email" + redirect-uris: + - "http://127.0.0.1:3333/oidc/callback" + flows: + authorization-code: + state: "" + client-credentials: + +authorization: + urls: + #identities: http://127.0.0.1:4434/admin/identities + trusted-issuers: http://127.0.0.1:4445/admin/trust/grants/jwt-bearer/issuers + login: http://127.0.0.1:4433/self-service/login/api + clients: http://127.0.0.1:4445/admin/clients + authorize: http://127.0.0.1:4444/oauth2/auth + register: http://127.0.0.1:4444/oauth2/register + token: http://127.0.0.1:4444/oauth2/token + + +options: + decode-id-token: true + decode-access-token: true + run-once: true + open-browser: false diff --git a/internal/authenticate.go b/internal/authenticate.go new file mode 100644 index 0000000..91f5940 --- /dev/null +++ b/internal/authenticate.go @@ -0,0 +1,118 @@ +package opaal + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strings" + + "github.com/davidallendj/go-utils/httpx" +) + +func (client *Client) IsFlowInitiated() bool { + return client.FlowId != "" +} + +func (client *Client) BuildAuthorizationUrl(issuer string, state string) string { + return issuer + "?" + "client_id=" + client.Id + + "&redirect_uri=" + url.QueryEscape(strings.Join(client.RedirectUris, ",")) + + "&response_type=code" + // this has to be set to "code" + "&state=" + state + + "&scope=" + strings.Join(client.Scope, "+") + + "&resource=" + url.QueryEscape("http://127.0.0.1:4444/oauth2/token") +} + +func (client *Client) InitiateLoginFlow(loginUrl string) error { + // kratos: GET /self-service/login/api + req, err := http.NewRequest("GET", loginUrl, bytes.NewBuffer([]byte{})) + if err != nil { + return fmt.Errorf("failed to make request: %v", err) + } + res, err := client.Do(req) + if err != nil { + return fmt.Errorf("failed to do request: %v", err) + } + defer res.Body.Close() + + // get the flow ID from response + body, err := io.ReadAll(res.Body) + if err != nil { + return fmt.Errorf("failed to read response body: %v", err) + } + + var flowData map[string]any + err = json.Unmarshal(body, &flowData) + if err != nil { + return fmt.Errorf("failed to unmarshal flow data: %v\n%v", err, string(body)) + } else { + client.FlowId = flowData["id"].(string) + } + return nil +} + +func (client *Client) FetchFlowData(url string) (map[string]any, error) { + //kratos: GET /self-service/login/flows?id={flowId} + + // replace {id} in string with actual value + url = strings.ReplaceAll(url, "{id}", client.FlowId) + _, b, err := httpx.MakeHttpRequest(url, http.MethodGet, nil, nil) + if err != nil { + return nil, fmt.Errorf("failed to make request: %v", err) + } + + var flowData map[string]any + err = json.Unmarshal(b, &flowData) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal flow data: %v", err) + } + return flowData, nil +} + +func (client *Client) FetchCSRFToken(flowUrl string) error { + data, err := client.FetchFlowData(flowUrl) + if err != nil { + return fmt.Errorf("failed to fetch flow data: %v", err) + } + + // iterate through nodes and extract the CSRF token attribute from the flow data + ui := data["ui"].(map[string]any) + nodes := ui["nodes"].([]any) + for _, node := range nodes { + attrs := node.(map[string]any)["attributes"].(map[string]any) + name := attrs["name"].(string) + if name == "csrf_token" { + client.CsrfToken = attrs["value"].(string) + return nil + } + } + return fmt.Errorf("failed to extract CSRF token: not found") +} + +func (client *Client) FetchTokenFromAuthenticationServer(code string, remoteUrl string, state string) ([]byte, error) { + body := url.Values{ + "grant_type": {"authorization_code"}, + "client_id": {client.Id}, + "client_secret": {client.Secret}, + "redirect_uri": {strings.Join(client.RedirectUris, ",")}, + } + // add optional params if valid + if code != "" { + body["code"] = []string{code} + } + if state != "" { + body["state"] = []string{state} + } + res, err := http.PostForm(remoteUrl, body) + if err != nil { + return nil, fmt.Errorf("failed to get ID token: %s", err) + } + defer res.Body.Close() + + // domain, _ := url.Parse("http://127.0.0.1") + // client.Jar.SetCookies(domain, res.Cookies()) + + return io.ReadAll(res.Body) +} diff --git a/internal/authorize.go b/internal/authorize.go new file mode 100644 index 0000000..8190b6d --- /dev/null +++ b/internal/authorize.go @@ -0,0 +1,241 @@ +package opaal + +import ( + "bytes" + "davidallendj/opaal/internal/oidc" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "slices" + "strings" + "time" + + "github.com/davidallendj/go-utils/httpx" + "github.com/davidallendj/go-utils/util" +) + +func (client *Client) AddTrustedIssuer(url string, idp *oidc.IdentityProvider, subject string, duration time.Duration) ([]byte, error) { + // hydra endpoint: POST /admin/trust/grants/jwt-bearer/issuers + if idp == nil { + return nil, fmt.Errorf("identity provided is nil") + } + jwkstr, err := json.Marshal(idp.Key) + if err != nil { + return nil, fmt.Errorf("failed to marshal JWK: %v", err) + } + quotedScopes := make([]string, len(client.Scope)) + for i, s := range client.Scope { + quotedScopes[i] = fmt.Sprintf("\"%s\"", s) + } + // NOTE: Can also include "jwks_uri" instead + data := []byte(fmt.Sprintf("{"+ + "\"allow_any_subject\": false,"+ + "\"issuer\": \"%s\","+ + "\"subject\": \"%s\","+ + "\"expires_at\": \"%v\","+ + "\"jwk\": %v,"+ + "\"scope\": [ %s ]"+ + "}", idp.Issuer, subject, time.Now().Add(duration).Format(time.RFC3339), string(jwkstr), strings.Join(quotedScopes, ","))) + + req, err := http.NewRequest("POST", url, bytes.NewBuffer(data)) + // req.Header.Add("X-CSRF-Token", client.CsrfToken.Value) + if err != nil { + return nil, fmt.Errorf("failed to make request: %v", err) + } + req.Header.Add("Content-Type", "application/json") + // req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", idToken)) + res, err := client.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to do request: %v", err) + } + defer res.Body.Close() + + return io.ReadAll(res.Body) +} + +func (client *Client) IsOAuthClientRegistered(clientUrl string) (bool, error) { + _, _, err := httpx.MakeHttpRequest(clientUrl, http.MethodGet, nil, nil) + if err != nil { + return false, fmt.Errorf("failed to make request: %v", err) + } + // TODO: need to check contents of actual response + return true, nil +} + +func (client *Client) GetOAuthClient(clientUrl string) error { + _, b, err := httpx.MakeHttpRequest(clientUrl, http.MethodGet, nil, nil) + if err != nil { + return fmt.Errorf("failed to make request: %v", err) + } + + fmt.Printf("GetOAuthClient: %v\n", string(b)) + + var data []map[string]any + err = json.Unmarshal(b, &data) + if err != nil { + return fmt.Errorf("failed to unmarshal JSON: %v", err) + } + + index := slices.IndexFunc(data, func(c map[string]any) bool { + if c["client_id"] == nil { + return false + } + return c["client_id"].(string) == client.Id + }) + if index < 0 { + return fmt.Errorf("client not found") + } + + // cast the redirect_uris from []any to []string and extract registration token + foundClient := data[index] + for _, uri := range foundClient["redirect_uris"].([]any) { + client.RedirectUris = append(client.RedirectUris, uri.(string)) + } + if foundClient["registration-access-token"] != nil { + client.RegistrationAccessToken = foundClient["registration-access-token"].(string) + } + + return nil +} + +func (client *Client) CreateOAuthClient(registerUrl string, audience []string) ([]byte, error) { + // hydra endpoint: POST /clients + audience = util.QuoteArrayStrings(audience) + body := httpx.Body(fmt.Sprintf(`{ + "client_id": "%s", + "client_name": "%s", + "client_secret": "%s", + "token_endpoint_auth_method": "client_secret_post", + "scope": "%s", + "grant_types": ["urn:ietf:params:oauth:grant-type:jwt-bearer"], + "response_types": ["token"], + "redirect_uris": ["http://127.0.0.1:3333/callback"], + "state": 12345678910, + "audience": [%s] + }`, client.Id, client.Id, client.Secret, strings.Join(client.Scope, " "), strings.Join(audience, ","), + )) + headers := httpx.Headers{ + "Content-Type": "application/json", + } + + _, b, err := httpx.MakeHttpRequest(registerUrl, http.MethodPost, []byte(body), headers) + if err != nil { + return nil, fmt.Errorf("failed to make request: %v", err) + } + var rjson map[string]any + err = json.Unmarshal(b, &rjson) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal response body: %v", err) + } + + // check for error first + errJson := rjson["error"] + if errJson == nil { + // set the client ID and secret of registered client + client.Id = rjson["client_id"].(string) + client.Secret = rjson["client_secret"].(string) + client.RegistrationAccessToken = rjson["registration_access_token"].(string) + } else { + return b, nil + } + + return b, err +} + +func (client *Client) RegisterOAuthClient(registerUrl string, audience []string) ([]byte, error) { + // hydra endpoint: POST /oauth2/register + audience = util.QuoteArrayStrings(audience) + body := httpx.Body(fmt.Sprintf(`{ + "client_name": "opaal", + "token_endpoint_auth_method": "client_secret_post", + "scope": "%s", + "grant_types": ["urn:ietf:params:oauth:grant-type:jwt-bearer"], + "response_types": ["token"], + "redirect_uris": ["http://127.0.0.1:3333/callback"], + "state": 12345678910, + "audience": [%s] + }`, strings.Join(client.Scope, " "), strings.Join(audience, ","), + )) + headers := httpx.Headers{ + "Content-Type": "application/json", + } + _, b, err := httpx.MakeHttpRequest(registerUrl, http.MethodPost, body, headers) + if err != nil { + return nil, fmt.Errorf("failed to make request: %v", err) + } + var rjson map[string]any + err = json.Unmarshal(b, &rjson) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal response body: %v", err) + } + + // check for error first + errJson := rjson["error"] + if errJson == nil { + // set the client ID and secret of registered client + client.Id = rjson["client_id"].(string) + client.Secret = rjson["client_secret"].(string) + client.RegistrationAccessToken = rjson["registration_access_token"].(string) + } else { + return b, nil + } + return b, err +} + +func (client *Client) AuthorizeOAuthClient(authorizeUrl string) ([]byte, error) { + // set the authorization header + body := []byte("grant_type=" + url.QueryEscape("urn:ietf:params:oauth:grant-type:jwt-bearer") + + "&scope=" + strings.Join(client.Scope, "+") + + "&client_id=" + client.Id + + "&client_secret=" + client.Secret + + "&redirect_uri=" + url.QueryEscape("http://127.0.0.1:3333/callback") + // FIXME: needs to not be hardcorded + "&response_type=token" + + "&state=12345678910", + ) + headers := httpx.Headers{ + "Authorization": "Bearer " + client.RegistrationAccessToken, + "Content-Type": "application/x-www-form-urlencoded", + } + _, b, err := httpx.MakeHttpRequest(authorizeUrl, http.MethodPost, body, headers) + if err != nil { + return nil, fmt.Errorf("failed to make HTTP request: %v", err) + } + + return b, nil +} + +func (client *Client) PerformTokenGrant(clientUrl string, jwt string) ([]byte, error) { + // hydra endpoint: /oauth/token + body := "grant_type=" + url.QueryEscape("urn:ietf:params:oauth:grant-type:jwt-bearer") + + "&client_id=" + client.Id + + "&client_secret=" + client.Secret + + "&redirect_uri=" + url.QueryEscape("http://127.0.0.1:3333/callback") + // add optional params if valid + if jwt != "" { + body += "&assertion=" + jwt + } + if client.Scope != nil || len(client.Scope) > 0 { + body += "&scope=" + strings.Join(client.Scope, "+") + } + headers := httpx.Headers{ + "Content-Type": "application/x-www-form-urlencoded", + "Authorization": "Bearer " + client.RegistrationAccessToken, + } + + _, b, err := httpx.MakeHttpRequest(clientUrl, http.MethodPost, []byte(body), headers) + + // set flow ID back to empty string to indicate a completed flow + client.FlowId = "" + + return b, err +} + +func (client *Client) DeleteOAuthClient(clientUrl string) error { + _, _, err := httpx.MakeHttpRequest(clientUrl+"/"+client.Id, http.MethodDelete, nil, nil) + if err != nil { + return fmt.Errorf("failed to make request: %v", err) + } + return nil +} diff --git a/internal/client.go b/internal/client.go index 48bb689..d9746f4 100644 --- a/internal/client.go +++ b/internal/client.go @@ -1,316 +1,90 @@ package opaal import ( - "bytes" - "davidallendj/opaal/internal/oidc" - "encoding/json" - "fmt" - "io" "net/http" "net/http/cookiejar" - "net/url" - "strings" - "time" + "slices" - "github.com/davidallendj/go-utils/httpx" - "github.com/davidallendj/go-utils/util" + "github.com/davidallendj/go-utils/mathx" "golang.org/x/net/publicsuffix" ) type Client struct { http.Client - Id string `yaml:"id"` - Secret string `yaml:"secret"` - RedirectUris []string `yaml:"redirect-uris"` - FlowId string - CsrfToken string + Id string `yaml:"id"` + Secret string `yaml:"secret"` + Name string `yaml:"name"` + Description string `yaml:"description"` + Issuer string `yaml:"issuer"` + RegistrationAccessToken string `yaml:"registration-access-token"` + RedirectUris []string `yaml:"redirect-uris"` + Scope []string `yaml:"scope"` + FlowId string + CsrfToken string +} + +func NewClient() *Client { + return &Client{} } func NewClientWithConfig(config *Config) *Client { - jar, _ := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List}) + // make sure config is valid + if config == nil { + return nil + } + + // make sure we have at least one client + clients := config.Authentication.Clients + if len(clients) <= 0 { + return nil + } + + // use the first client found by default return &Client{ - Id: config.Client.Id, - Secret: config.Client.Secret, - RedirectUris: config.Client.RedirectUris, - Client: http.Client{Jar: jar}, + Id: clients[0].Id, + Secret: clients[0].Secret, + Name: clients[0].Name, + Issuer: clients[0].Issuer, + Scope: clients[0].Scope, + RedirectUris: clients[0].RedirectUris, } } -func (client *Client) IsFlowInitiated() bool { - return client.FlowId != "" +func NewClientWithConfigByIndex(config *Config, index int) *Client { + size := len(config.Authentication.Clients) + index = mathx.Clamp(index, 0, size) + return nil } -func (client *Client) BuildAuthorizationUrl(authEndpoint string, state string, responseType string, scope []string) string { - return authEndpoint + "?" + "client_id=" + client.Id + - "&redirect_uri=" + url.QueryEscape(strings.Join(client.RedirectUris, ",")) + - "&response_type=" + responseType + - "&state=" + state + - "&scope=" + strings.Join(scope, "+") + - "&audience=http://127.0.0.1:4444/oauth2/token" -} - -func (client *Client) InitiateLoginFlow(loginUrl string) error { - // kratos: GET /self-service/login/api - req, err := http.NewRequest("GET", loginUrl, bytes.NewBuffer([]byte{})) - if err != nil { - return fmt.Errorf("failed to make request: %v", err) - } - res, err := client.Do(req) - if err != nil { - return fmt.Errorf("failed to do request: %v", err) - } - defer res.Body.Close() - - // get the flow ID from response - body, err := io.ReadAll(res.Body) - if err != nil { - return fmt.Errorf("failed to read response body: %v", err) - } - - var flowData map[string]any - err = json.Unmarshal(body, &flowData) - if err != nil { - return fmt.Errorf("failed to unmarshal flow data: %v\n%v", err, string(body)) - } else { - client.FlowId = flowData["id"].(string) +func NewClientWithConfigByName(config *Config, name string) *Client { + index := slices.IndexFunc(config.Authentication.Clients, func(c Client) bool { + return c.Name == name + }) + if index >= 0 { + return &config.Authentication.Clients[index] } return nil } -func (client *Client) FetchFlowData(flowUrl string) (map[string]any, error) { - //kratos: GET /self-service/login/flows?id={flowId} +func NewClientWithConfigByProvider(config *Config, issuer string) *Client { + index := slices.IndexFunc(config.Authentication.Clients, func(c Client) bool { + return c.Issuer == issuer + }) - // replace {id} in string with actual value - flowUrl = strings.ReplaceAll(flowUrl, "{id}", client.FlowId) - req, err := http.NewRequest("GET", flowUrl, nil) - if err != nil { - return nil, fmt.Errorf("failed to make request: %v", err) + if index >= 0 { + return &config.Authentication.Clients[index] } - res, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("failed to do request: %v", err) - } - defer res.Body.Close() - - // get the flow data from response - body, err := io.ReadAll(res.Body) - if err != nil { - return nil, fmt.Errorf("failed to read response body: %v", err) - } - - var flowData map[string]any - err = json.Unmarshal(body, &flowData) - if err != nil { - return nil, fmt.Errorf("failed to unmarshal flow data: %v", err) - } - return flowData, nil + return nil } -func (client *Client) FetchCSRFToken(flowUrl string) error { - data, err := client.FetchFlowData(flowUrl) - if err != nil { - return fmt.Errorf("failed to fetch flow data: %v", err) +func NewClientWithConfigById(config *Config, id string) *Client { + index := slices.IndexFunc(config.Authentication.Clients, func(c Client) bool { + return c.Id == id + }) + if index >= 0 { + return &config.Authentication.Clients[index] } - - // iterate through nodes and extract the CSRF token attribute from the flow data - ui := data["ui"].(map[string]any) - nodes := ui["nodes"].([]any) - for _, node := range nodes { - attrs := node.(map[string]any)["attributes"].(map[string]any) - name := attrs["name"].(string) - if name == "csrf_token" { - client.CsrfToken = attrs["value"].(string) - return nil - } - } - return fmt.Errorf("failed to extract CSRF token: not found") -} - -func (client *Client) FetchTokenFromAuthenticationServer(code string, remoteUrl string, state string) ([]byte, error) { - data := url.Values{ - "grant_type": {"authorization_code"}, - "client_id": {client.Id}, - "client_secret": {client.Secret}, - "redirect_uri": {strings.Join(client.RedirectUris, ",")}, - } - // add optional params if valid - if code != "" { - data["code"] = []string{code} - } - if state != "" { - data["state"] = []string{state} - } - res, err := http.PostForm(remoteUrl, data) - if err != nil { - return nil, fmt.Errorf("failed to get ID token: %s", err) - } - defer res.Body.Close() - - domain, _ := url.Parse("http://127.0.0.1") - client.Jar.SetCookies(domain, res.Cookies()) - - return io.ReadAll(res.Body) -} - -func (client *Client) FetchTokenFromAuthorizationServer(remoteUrl string, jwt string, scope []string) ([]byte, error) { - // hydra endpoint: /oauth/token - data := "grant_type=" + url.QueryEscape("urn:ietf:params:oauth:grant-type:jwt-bearer") + - "&client_id=" + client.Id + - "&client_secret=" + client.Secret - - // add optional params if valid - if jwt != "" { - data += "&assertion=" + jwt - } - if scope != nil || len(scope) > 0 { - data += "&scope=" + strings.Join(scope, "+") - } - - fmt.Printf("encoded params: %v\n\n", data) - req, err := http.NewRequest("POST", remoteUrl, bytes.NewBuffer([]byte(data))) - req.Header.Add("Content-Type", "application/x-www-form-urlencoded") - if err != nil { - return nil, fmt.Errorf("failed to make request: %s", err) - } - res, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("failed to do request: %v", err) - } - defer res.Body.Close() - - // set flow ID back to empty string to indicate a completed flow - client.FlowId = "" - - return io.ReadAll(res.Body) -} - -func (client *Client) AddTrustedIssuer(remoteUrl string, idp *oidc.IdentityProvider, subject string, duration time.Duration, scope []string) ([]byte, error) { - // hydra endpoint: POST /admin/trust/grants/jwt-bearer/issuers - if idp == nil { - return nil, fmt.Errorf("identity provided is nil") - } - jwkstr, err := json.Marshal(idp.Key) - if err != nil { - return nil, fmt.Errorf("failed to marshal JWK: %v", err) - } - quotedScopes := make([]string, len(scope)) - for i, s := range scope { - quotedScopes[i] = fmt.Sprintf("\"%s\"", s) - } - // NOTE: Can also include "jwks_uri" instead - data := []byte(fmt.Sprintf("{"+ - "\"allow_any_subject\": false,"+ - "\"issuer\": \"%s\","+ - "\"subject\": \"%s\","+ - "\"expires_at\": \"%v\","+ - "\"jwk\": %v,"+ - "\"scope\": [ %s ]"+ - "}", idp.Issuer, subject, time.Now().Add(duration).Format(time.RFC3339), string(jwkstr), strings.Join(quotedScopes, ","))) - fmt.Printf("%v\n", string(data)) - - req, err := http.NewRequest("POST", remoteUrl, bytes.NewBuffer(data)) - // req.Header.Add("X-CSRF-Token", client.CsrfToken.Value) - if err != nil { - return nil, fmt.Errorf("failed to make request: %v", err) - } - req.Header.Add("Content-Type", "application/json") - // req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", idToken)) - res, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("failed to do request: %v", err) - } - defer res.Body.Close() - - return io.ReadAll(res.Body) -} - -func (client *Client) AuthorizeClient(authorizeUrl string) ([]byte, error) { - // encode ID and secret for authorization header basic authentication - basicAuth := util.EncodeBase64( - fmt.Sprintf("%s:%s", - url.QueryEscape(client.Id), - url.QueryEscape(client.Secret), - ), - ) - body := httpx.Body("grant_type=client_credentials&scope=read") - headers := httpx.Headers{ - "Authorization": basicAuth, - "Content-Type": "application/x-www-form-urlencoded", - } - _, b, err := httpx.MakeHTTPRequest(authorizeUrl, http.MethodPost, body, headers) - if err != nil { - return nil, fmt.Errorf("failed to make HTTP request: %v", err) - } - - return b, nil -} - -func (client *Client) RegisterOAuthClient(registerUrl string, audience []string) ([]byte, error) { - // hydra endpoint: POST /clients - audience = util.QuoteArrayStrings(audience) - data := []byte(fmt.Sprintf(`{ - "client_name": "%s", - "client_secret": "%s", - "token_endpoint_auth_method": "client_secret_post", - "scope": "openid email profile", - "grant_types": ["client_credentials", "urn:ietf:params:oauth:grant-type:jwt-bearer"], - "response_types": ["token"], - }`, client.Id, client.Secret, - // strings.Join(audience, ",") - )) - // "audience": [%s] - - req, err := http.NewRequest("POST", registerUrl, bytes.NewBuffer(data)) - if err != nil { - return nil, fmt.Errorf("failed to make request: %v", err) - } - req.Header.Add("Content-Type", "application/json") - // req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", idToken)) - res, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("failed to do request: %v", err) - } - defer res.Body.Close() - - return io.ReadAll(res.Body) -} - -func (client *Client) CreateIdentity(remoteUrl string, idToken string) ([]byte, error) { - // kratos endpoint: /admin/identities - data := []byte(`{ - "schema_id": "preset://email", - "traits": { - "email": "docs@example.org" - } - }`) - - req, err := http.NewRequest("POST", remoteUrl, bytes.NewBuffer(data)) - if err != nil { - return nil, 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)) - // req.Header.Add("X-CSRF-Token", client.CsrfToken.Value) - res, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("failed to do request: %v", err) - } - - return io.ReadAll(res.Body) -} - -func (client *Client) FetchIdentities(remoteUrl string) ([]byte, error) { - req, err := http.NewRequest("GET", remoteUrl, bytes.NewBuffer([]byte{})) - if err != nil { - return nil, fmt.Errorf("failed to create a new request: %v", err) - } - - res, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("failed to do request: %v", err) - } - - return io.ReadAll(res.Body) + return nil } func (client *Client) ClearCookies() { diff --git a/internal/config.go b/internal/config.go index f0f5d49..362f151 100644 --- a/internal/config.go +++ b/internal/config.go @@ -1,7 +1,6 @@ package opaal import ( - "davidallendj/opaal/internal/oidc" "log" "os" "path/filepath" @@ -11,20 +10,46 @@ import ( "gopkg.in/yaml.v2" ) +type FlowOptions map[string]string +type Flows map[string]FlowOptions +type Providers map[string]string + +type Options struct { + DecodeIdToken bool `yaml:"decode-id-token"` + DecodeAccessToken bool `yaml:"decode-access-token"` + RunOnce bool `yaml:"run-once"` + OpenBrowser bool `yaml:"open-browser"` + FlowType string `yaml:"flow"` + CachePath string `yaml:"cache"` + LocalOnly bool `yaml:"local-only"` +} + +type RequestUrls struct { + Identities string `yaml:"identities"` + TrustedIssuers string `yaml:"trusted-issuers"` + Login string `yaml:"login"` + Clients string `yaml:"clients"` + Token string `yaml:"token"` + Authorize string `yaml:"authorize"` + Register string `yaml:"register"` +} + +type Authentication struct { + Clients []Client `yaml:"clients"` + Flows Flows `yaml:"flows"` +} + +type Authorization struct { + RequestUrls RequestUrls `yaml:"urls"` +} + type Config struct { - Version string `yaml:"version"` - Server Server `yaml:"server"` - Client Client `yaml:"client"` - IdentityProvider oidc.IdentityProvider `yaml:"oidc"` - State string `yaml:"state"` - ResponseType string `yaml:"response-type"` - Scope []string `yaml:"scope"` - ActionUrls ActionUrls `yaml:"urls"` - OpenBrowser bool `yaml:"open-browser"` - DecodeIdToken bool `yaml:"decode-id-token"` - DecodeAccessToken bool `yaml:"decode-access-token"` - RunOnce bool `yaml:"run-once"` - GrantType string `yaml:"grant-type"` + Version string `yaml:"version"` + Server Server `yaml:"server"` + Providers Providers `yaml:"providers"` + Options Options `yaml:"options"` + Authentication Authentication `yaml:"authentication"` + Authorization Authorization `yaml:"authorization"` } func NewConfig() Config { @@ -34,31 +59,17 @@ func NewConfig() Config { Host: "127.0.0.1", Port: 3333, }, - Client: Client{ - Id: "", - Secret: "", - RedirectUris: []string{""}, + Options: Options{ + DecodeIdToken: true, + DecodeAccessToken: true, + RunOnce: true, + OpenBrowser: false, + CachePath: "opaal.db", + FlowType: "authorization_code", + LocalOnly: false, }, - IdentityProvider: *oidc.NewIdentityProvider(), - State: goutil.RandomString(20), - ResponseType: "code", - Scope: []string{"openid", "profile", "email"}, - ActionUrls: ActionUrls{ - Identities: "", - AccessToken: "", - TrustedIssuers: "", - ServerConfig: "", - JwksUri: "", - Login: "", - LoginFlowId: "", - RegisterClient: "", - AuthorizeClient: "", - }, - OpenBrowser: false, - DecodeIdToken: false, - DecodeAccessToken: false, - RunOnce: true, - GrantType: "authorization_code", + Authentication: Authentication{}, + Authorization: Authorization{}, } } @@ -94,3 +105,15 @@ func SaveDefaultConfig(path string) { return } } + +func HasRequiredConfigParams(config *Config) bool { + // must have athe requirements to perform login + hasClients := len(config.Authentication.Clients) > 0 + hasServer := config.Server.Host != "" && config.Server.Port != 0 && config.Server.Callback != "" + hasEndpoints := config.Authorization.RequestUrls.TrustedIssuers != "" && + config.Authorization.RequestUrls.Login != "" && + config.Authorization.RequestUrls.Clients != "" && + config.Authorization.RequestUrls.Authorize != "" && + config.Authorization.RequestUrls.Token != "" + return hasClients && hasServer && hasEndpoints +} diff --git a/internal/flows/authorization_code.go b/internal/flows/authorization_code.go deleted file mode 100644 index cf5d5a3..0000000 --- a/internal/flows/authorization_code.go +++ /dev/null @@ -1,254 +0,0 @@ -package flows - -import ( - opaal "davidallendj/opaal/internal" - "davidallendj/opaal/internal/oidc" - "encoding/json" - "errors" - "fmt" - "net/http" - "reflect" - "time" - - "github.com/davidallendj/go-utils/util" -) - -func AuthorizationCode(config *opaal.Config, server *opaal.Server, client *opaal.Client) error { - // initiate the login flow and get a flow ID and CSRF token - { - err := client.InitiateLoginFlow(config.ActionUrls.Login) - if err != nil { - return fmt.Errorf("failed to initiate login flow: %v", err) - } - err = client.FetchCSRFToken(config.ActionUrls.LoginFlowId) - if err != nil { - return fmt.Errorf("failed to fetch CSRF token: %v", err) - } - } - - // try and fetch server configuration if provided URL - idp := oidc.NewIdentityProvider() - if config.ActionUrls.ServerConfig != "" { - fmt.Printf("Fetching server configuration: %s\n", config.ActionUrls.ServerConfig) - err := idp.FetchServerConfig(config.ActionUrls.ServerConfig) - if err != nil { - return fmt.Errorf("failed to fetch server config: %v", err) - } - } else { - // otherwise, use what's provided in config file - idp.Issuer = config.IdentityProvider.Issuer - idp.Endpoints = config.IdentityProvider.Endpoints - idp.Supported = config.IdentityProvider.Supported - } - - // check if all appropriate parameters are set in config - if !opaal.HasRequiredParams(config) { - return fmt.Errorf("client ID must be set") - } - - // build the authorization URL to redirect user for social sign-in - var authorizationUrl = client.BuildAuthorizationUrl( - idp.Endpoints.Authorization, - config.State, - config.ResponseType, - config.Scope, - ) - - // print the authorization URL for sharing - fmt.Printf("Login with identity provider:\n\n %s/login\n %s\n\n", - server.GetListenAddr(), authorizationUrl, - ) - - // automatically open browser to initiate login flow (only useful for testing) - if config.OpenBrowser { - util.OpenUrl(authorizationUrl) - } - - // authorize oauth client and listen for callback from provider - fmt.Printf("Waiting for authorization code redirect @%s/oidc/callback...\n", server.GetListenAddr()) - code, err := server.WaitForAuthorizationCode(authorizationUrl) - if errors.Is(err, http.ErrServerClosed) { - fmt.Printf("\n=========================================\nServer closed.\n=========================================\n\n") - } else if err != nil { - return fmt.Errorf("failed to start server: %s", err) - } - - if client == nil { - fmt.Printf("client did not initialize\n") - } - - // start up another serve in background to listen for success or failures - d := make(chan []byte) - quit := make(chan bool) - var access_token []byte - go server.Serve(d) - go func() { - select { - case <-d: - fmt.Printf("got access token") - quit <- true - case <-quit: - close(d) - close(quit) - return - default: - } - }() - - // use code from response and exchange for bearer token (with ID token) - bearerToken, err := client.FetchTokenFromAuthenticationServer( - code, - idp.Endpoints.Token, - config.State, - ) - if err != nil { - return fmt.Errorf("failed to fetch token from issuer: %v", err) - } - - // unmarshal data to get id_token and access_token - var data map[string]any - err = json.Unmarshal([]byte(bearerToken), &data) - if err != nil || data == nil { - return fmt.Errorf("failed to unmarshal token: %v", err) - } - - // extract ID token from bearer as JSON string for easy consumption - idToken := data["id_token"].(string) - idJwtSegments, err := util.DecodeJwt(idToken) - if err != nil { - fmt.Printf("failed to parse ID token: %v\n", err) - } else { - fmt.Printf("id_token: %v\n", idToken) - if config.DecodeIdToken { - if err != nil { - fmt.Printf("failed to decode JWT: %v\n", err) - } else { - for i, segment := range idJwtSegments { - // don't print last segment (signatures) - if i == len(idJwtSegments)-1 { - break - } - fmt.Printf("%s\n", string(segment)) - } - } - } - fmt.Println() - } - - // extract the access token to get the scopes - accessToken := data["access_token"].(string) - accessJwtSegments, err := util.DecodeJwt(accessToken) - if err != nil || len(accessJwtSegments) <= 0 { - fmt.Printf("failed to parse access token: %v\n", err) - } else { - fmt.Printf("access_token: %v\n", accessToken) - if config.DecodeIdToken { - if err != nil { - fmt.Printf("failed to decode JWT: %v\n", err) - } else { - for i, segment := range accessJwtSegments { - // don't print last segment (signatures) - if i == len(accessJwtSegments)-1 { - break - } - fmt.Printf("%s\n", string(segment)) - } - } - } - fmt.Println() - } - - // extract the scope from access token claims - // var scope []string - // var accessJsonPayload map[string]any - // var accessJwtPayload []byte = accessJwtSegments[1] - // if accessJsonPayload != nil { - // err := json.Unmarshal(accessJwtPayload, &accessJsonPayload) - // if err != nil { - // return fmt.Errorf("failed to unmarshal JWT: %v", err) - // } - // scope = idJsonPayload["scope"].([]string) - // } - - // create a new identity with identity and session manager if url is provided - if config.ActionUrls.Identities != "" { - fmt.Printf("Attempting to create a new identity...\n") - _, err := client.CreateIdentity(config.ActionUrls.Identities, idToken) - if err != nil { - return fmt.Errorf("failed to create new identity: %v", err) - } - _, err = client.FetchIdentities(config.ActionUrls.Identities) - if err != nil { - return fmt.Errorf("failed to fetch identities: %v", err) - } - fmt.Printf("Created new identity successfully.\n\n") - } - - // extract the subject from ID token claims - var subject string - var audience []string - var idJsonPayload map[string]any - var idJwtPayload []byte = idJwtSegments[1] - if idJwtPayload != nil { - err := json.Unmarshal(idJwtPayload, &idJsonPayload) - if err != nil { - return fmt.Errorf("failed to unmarshal JWT: %v", err) - } - subject = idJsonPayload["sub"].(string) - audType := reflect.ValueOf(idJsonPayload["aud"]) - switch audType.Kind() { - case reflect.String: - audience = append(audience, idJsonPayload["aud"].(string)) - case reflect.Array: - audience = idJsonPayload["aud"].([]string) - } - } else { - return fmt.Errorf("failed to extract subject from ID token claims") - } - - // fetch JWKS and add issuer to authentication server to submit ID token - fmt.Printf("Fetching JWKS from authentication server for verification...\n") - err = idp.FetchJwk(config.ActionUrls.JwksUri) - if err != nil { - return fmt.Errorf("failed to fetch JWK: %v", err) - } else { - fmt.Printf("Successfully retrieved JWK from authentication server.\n\n") - fmt.Printf("Attempting to add issuer to authorization server...\n") - res, err := client.AddTrustedIssuer(config.ActionUrls.TrustedIssuers, idp, subject, time.Duration(1000), config.Scope) - if err != nil { - return fmt.Errorf("failed to add trusted issuer: %v", err) - } - fmt.Printf("%v\n", string(res)) - } - - // try and register a new client with authorization server - fmt.Printf("Registering new OAuth2 client with authorization server...\n") - res, err := client.RegisterOAuthClient("http://127.0.0.1:4445/clients", audience) - if err != nil { - return fmt.Errorf("failed to register client: %v", err) - } - fmt.Printf("%v\n", string(res)) - - // extract the client info from response - var clientData map[string]any - err = json.Unmarshal(res, &clientData) - if err != nil { - return fmt.Errorf("failed to unmarshal client data: %v", err) - } else { - client.Id = clientData["client_id"].(string) - client.Secret = clientData["client_secret"].(string) - } - - // use ID token/user info to fetch access token from authentication server - if config.ActionUrls.AccessToken != "" { - fmt.Printf("Fetching access token from authorization server...\n") - res, err := client.FetchTokenFromAuthorizationServer(config.ActionUrls.AccessToken, idToken, config.Scope) - if err != nil { - return fmt.Errorf("failed to fetch access token: %v", err) - } - fmt.Printf("%s\n", res) - } - - d <- access_token - return nil -} diff --git a/internal/flows/client_credentials.go b/internal/flows/client_credentials.go deleted file mode 100644 index 6c59037..0000000 --- a/internal/flows/client_credentials.go +++ /dev/null @@ -1,29 +0,0 @@ -package flows - -import ( - opaal "davidallendj/opaal/internal" - "fmt" -) - -func ClientCredentials(config *opaal.Config, server *opaal.Server, client *opaal.Client) error { - // register a new OAuth 2 client with authorization srever - _, err := client.RegisterOAuthClient(config.ActionUrls.RegisterClient, nil) - if err != nil { - return fmt.Errorf("failed to register OAuth client: %v", err) - } - - // authorize the client - _, err = client.AuthorizeClient(config.ActionUrls.AuthorizeClient) - if err != nil { - return fmt.Errorf("failed to authorize client: %v", err) - } - - // request a token from the authorization server - res, err := client.FetchTokenFromAuthorizationServer(config.ActionUrls.AccessToken, "", nil) - if err != nil { - return fmt.Errorf("failed to fetch token from authorization server: %v", err) - } - - fmt.Printf("token: %v\n", string(res)) - return nil -} diff --git a/internal/flows/login.go b/internal/flows/login.go deleted file mode 100644 index 1ee953b..0000000 --- a/internal/flows/login.go +++ /dev/null @@ -1,28 +0,0 @@ -package flows - -import ( - opaal "davidallendj/opaal/internal" - "fmt" -) - -func Login(config *opaal.Config) error { - if config == nil { - return fmt.Errorf("config is not valid") - } - - // initialize client that will be used throughout login flow - server := opaal.NewServerWithConfig(config) - client := opaal.NewClientWithConfig(config) - - fmt.Printf("grant type: %v\n", config.GrantType) - - if config.GrantType == "authorization_code" { - AuthorizationCode(config, server, client) - } else if config.GrantType == "client_credentials" { - ClientCredentials(config, server, client) - } else { - return fmt.Errorf("invalid grant type") - } - - return nil -} diff --git a/internal/identities.go b/internal/identities.go new file mode 100644 index 0000000..e1ddee5 --- /dev/null +++ b/internal/identities.go @@ -0,0 +1,32 @@ +package opaal + +import ( + "fmt" + "net/http" + + "github.com/davidallendj/go-utils/httpx" +) + +func (client *Client) CreateIdentity(url string, idToken string) error { + // kratos endpoint: /admin/identities + body := []byte(`{ + "schema_id": "preset://email", + "traits": { + "email": "docs@example.org" + } + }`) + headers := httpx.Headers{ + "Content-Type": "application/json", + "Authorization": fmt.Sprintf("Bearer %s", idToken), + } + _, _, err := httpx.MakeHttpRequest(url, http.MethodPost, body, headers) + if err != nil { + return fmt.Errorf("failed to read response body: %v", err) + } + return nil +} + +func (client *Client) FetchIdentities(remoteUrl string) ([]byte, error) { + _, b, err := httpx.MakeHttpRequest(remoteUrl, http.MethodGet, []byte{}, httpx.Headers{}) + return b, err +} diff --git a/internal/login.go b/internal/login.go new file mode 100644 index 0000000..686dbed --- /dev/null +++ b/internal/login.go @@ -0,0 +1,37 @@ +package opaal + +import ( + "davidallendj/opaal/internal/db" + "davidallendj/opaal/internal/oidc" + "fmt" +) + +func Login(config *Config, client *Client, provider *oidc.IdentityProvider) error { + if config == nil { + return fmt.Errorf("config is not valid") + } + + // make cache if it's not where expect + _, err := db.CreateIdentityProvidersIfNotExists(config.Options.CachePath) + if err != nil { + fmt.Printf("failed to create cache: %v\n", err) + } + + if config.Options.FlowType == "authorization_code" { + // create a server if doing authorization code flow + server := NewServerWithConfig(config) + err := AuthorizationCodeWithConfig(config, server, client, provider) + if err != nil { + fmt.Printf("failed to complete authorization code flow: %v\n", err) + } + } else if config.Options.FlowType == "client_credentials" { + err := ClientCredentialsWithConfig(config, client) + if err != nil { + fmt.Printf("failed to complete client credentials flow: %v", err) + } + } else { + return fmt.Errorf("invalid grant type (options: authorization_code, client_credentials)") + } + + return nil +} diff --git a/internal/oidc/oidc.go b/internal/oidc/oidc.go index e17a120..4f509a9 100644 --- a/internal/oidc/oidc.go +++ b/internal/oidc/oidc.go @@ -13,29 +13,29 @@ import ( ) type IdentityProvider struct { - Issuer string `json:"issuer" yaml:"issuer"` - Endpoints Endpoints `json:"endpoints" yaml:"endpoints"` - Supported Supported `json:"supported" yaml:"supported"` + Issuer string `db:"issuer" json:"issuer" yaml:"issuer"` + Endpoints Endpoints `db:"endpoints" json:"endpoints" yaml:"endpoints"` + Supported Supported `db:"supported" json:"supported" yaml:"supported"` Key jwk.Key } type Endpoints struct { - Authorization string `json:"authorization_endpoint" yaml:"authorization"` - Token string `json:"token_endpoint" yaml:"token"` - Revocation string `json:"revocation_endpoint" yaml:"revocation"` - Introspection string `json:"introspection_endpoint" yaml:"introspection"` - UserInfo string `json:"userinfo_endpoint" yaml:"userinfo"` - Jwks string `json:"jwks_uri" yaml:"jwks_uri"` + Authorization string `db:"authorization_endpoint" json:"authorization_endpoint" yaml:"authorization"` + Token string `db:"token_endpoint" json:"token_endpoint" yaml:"token"` + Revocation string `db:"revocation_endpoint" json:"revocation_endpoint" yaml:"revocation"` + Introspection string `db:"introspection_endpoint" json:"introspection_endpoint" yaml:"introspection"` + UserInfo string `db:"userinfo_endpoint" json:"userinfo_endpoint" yaml:"userinfo"` + Jwks string `db:"jwks_uri" json:"jwks_uri" yaml:"jwks_uri"` } type Supported struct { - ResponseTypes []string `json:"response_types_supported"` - ResponseModes []string `json:"response_modes_supported"` - GrantTypes []string `json:"grant_types_supported"` - TokenEndpointAuthMethods []string `json:"token_endpoint_auth_methods_supported"` - SubjectTypes []string `json:"subject_types_supported"` - IdTokenSigningAlgValues []string `json:"id_token_signing_alg_values_supported"` - ClaimTypes []string `json:"claim_types_supported"` - Claims []string `json:"claims_supported"` + ResponseTypes []string `db:"response_types_supported" json:"response_types_supported"` + ResponseModes []string `db:"response_modes_supported" json:"response_modes_supported"` + GrantTypes []string `db:"grant_types_supported" json:"grant_types_supported"` + TokenEndpointAuthMethods []string `db:"token_endpoint_auth_methods_supported" json:"token_endpoint_auth_methods_supported"` + SubjectTypes []string `db:"subject_types_supported" json:"subject_types_supported"` + IdTokenSigningAlgValues []string `db:"id_token_signing_alg_values_supported" json:"id_token_signing_alg_values_supported"` + ClaimTypes []string `db:"claim_types_supported" json:"claim_types_supported"` + Claims []string `db:"claims_supported" json:"claims_supported"` } func NewIdentityProvider() *IdentityProvider { @@ -109,38 +109,39 @@ func (p *IdentityProvider) LoadServerConfig(path string) error { return nil } -func (p *IdentityProvider) FetchServerConfig(url string) error { +func FetchServerConfig(issuer string) (*IdentityProvider, error) { // make a request to a server's openid-configuration - req, err := http.NewRequest("GET", url, bytes.NewBuffer([]byte{})) + req, err := http.NewRequest(http.MethodGet, issuer+"/.well-known/openid-configuration", bytes.NewBuffer([]byte{})) if err != nil { - return fmt.Errorf("failed to create a new request: %v", err) + return nil, fmt.Errorf("failed to create a new request: %v", err) } client := &http.Client{} // temp client to get info and not used in flow res, err := client.Do(req) if err != nil { - return fmt.Errorf("failed to do request: %v", err) + return nil, fmt.Errorf("failed to do request: %v", err) } body, err := io.ReadAll(res.Body) if err != nil { - return fmt.Errorf("failed to read response body: %v", err) + return nil, fmt.Errorf("failed to read response body: %v", err) } + var p IdentityProvider err = p.ParseServerConfig(body) if err != nil { - return fmt.Errorf("failed to parse server config: %v", err) + return nil, fmt.Errorf("failed to parse server config: %v", err) } - return nil + return &p, nil } -func (p *IdentityProvider) FetchJwk(url string) error { - if url == "" { - url = p.Endpoints.Jwks +func (p *IdentityProvider) FetchJwk() error { + if p.Endpoints.Jwks == "" { + return fmt.Errorf("JWKS endpoint not set") } // fetch JWKS from identity provider ctx, cancel := context.WithCancel(context.Background()) defer cancel() - set, err := jwk.Fetch(ctx, url) + set, err := jwk.Fetch(ctx, p.Endpoints.Jwks) if err != nil { return fmt.Errorf("%v", err) } diff --git a/internal/opaal.go b/internal/opaal.go deleted file mode 100644 index ee9cb11..0000000 --- a/internal/opaal.go +++ /dev/null @@ -1,17 +0,0 @@ -package opaal - -type ActionUrls struct { - Identities string `yaml:"identities"` - TrustedIssuers string `yaml:"trusted-issuers"` - AccessToken string `yaml:"access-token"` - ServerConfig string `yaml:"server-config"` - JwksUri string `yaml:"jwks_uri"` - Login string `yaml:"login"` - LoginFlowId string `yaml:"login-flow-id"` - RegisterClient string `yaml:"register-client"` - AuthorizeClient string `yaml:"authorize-client"` -} - -func HasRequiredParams(config *Config) bool { - return config.Client.Id != "" && config.Client.Secret != "" -} diff --git a/internal/server.go b/internal/server.go index c048592..4984902 100644 --- a/internal/server.go +++ b/internal/server.go @@ -12,13 +12,14 @@ import ( type Server struct { *http.Server - Host string `yaml:"host"` - Port int `yaml:"port"` + Host string `yaml:"host"` + Port int `yaml:"port"` + Callback string `yaml:"callback"` } -func NewServerWithConfig(config *Config) *Server { - host := config.Server.Host - port := config.Server.Port +func NewServerWithConfig(conf *Config) *Server { + host := conf.Server.Host + port := conf.Server.Port server := &Server{ Server: &http.Server{ Addr: fmt.Sprintf("%s:%d", host, port), @@ -37,7 +38,12 @@ func (s *Server) GetListenAddr() string { return fmt.Sprintf("%s:%d", s.Host, s.Port) } -func (s *Server) WaitForAuthorizationCode(loginUrl string) (string, error) { +func (s *Server) WaitForAuthorizationCode(loginUrl string, callback string) (string, error) { + // check if callback is set + if callback == "" { + callback = "/oidc/callback" + } + var code string r := chi.NewRouter() r.Use(middleware.RedirectSlashes) @@ -53,7 +59,7 @@ func (s *Server) WaitForAuthorizationCode(loginUrl string) (string, error) { loginPage = []byte(strings.ReplaceAll(string(loginPage), "{{loginUrl}}", loginUrl)) w.Write(loginPage) }) - r.HandleFunc("/oidc/callback", func(w http.ResponseWriter, r *http.Request) { + r.HandleFunc(callback, func(w http.ResponseWriter, r *http.Request) { // get the code from the OIDC provider if r != nil { code = r.URL.Query().Get("code")