commit f8f90c896c41cf1c1d1632b867c0d3d6c36e52a1 Author: kekskurse Date: Thu Sep 12 19:39:24 2024 +0200 chore: oAuth Client get Authentification URL diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..22d0d82 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +vendor diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000..915eccf --- /dev/null +++ b/Readme.md @@ -0,0 +1,4 @@ +Small oAuth2 Client to have an easy way to connect to Authentik + +# Links +* https://www.oauth.com/oauth2-servers/authorization/the-authorization-request/ diff --git a/auth.go b/auth.go new file mode 100644 index 0000000..a1cb832 --- /dev/null +++ b/auth.go @@ -0,0 +1,84 @@ +package kekskurseauth + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strings" +) + +type Auth struct { + config AuthConfig + clientID string + clientSecret string +} + +func NewAuthWithConfig(config AuthConfig, clientID, clientSecret string) (Auth, error) { + a := Auth{} + a.config = config + a.clientID = clientID + a.clientSecret = clientSecret + return a, nil +} + +func NewAuthWithConfigurationURL(url, clientID, clientSecret string) (Auth, error) { + a := Auth{} + a.clientID = clientID + a.clientSecret = clientSecret + config := AuthConfig{} + + res, err := http.Get(url) + if err != nil { + return Auth{}, fmt.Errorf("%w: %q", ErrCantGetConfiguratorData, err) + } + defer res.Body.Close() + + bodyContent, err := io.ReadAll(res.Body) + if err != nil { + return Auth{}, fmt.Errorf("%w: %q", ErrCantGetConfiguratorData, err) + } + + err = json.Unmarshal(bodyContent, &config) + if err != nil { + return Auth{}, fmt.Errorf("%w: %q", ErrCantGetConfiguratorData, err) + } + + a.config = config + return a, nil +} + +func (a Auth) GetAuthorizationURL(redirectUrl string, scope []string, state string) (string, error) { + if a.config.AuthorizationEndpoint == "" { + return "", fmt.Errorf("%w: %s", ErrCantGetAuthorizationURL, "AuthorizationEndpoint in config is empty") + } + + if a.clientID == "" { + return "", fmt.Errorf("%w: %s", ErrCantGetAuthorizationURL, "clientid in config is empty") + } + + url, err := url.Parse(a.config.AuthorizationEndpoint) + if err != nil { + return "", fmt.Errorf("%w: %q", ErrCantGetAuthorizationURL, err) + } + + values := url.Query() + + values.Set("client_id", a.clientID) + if redirectUrl != "" { + values.Set("redirect_uri", redirectUrl) + } + + if len(scope) > 0 { + values.Set("scope", strings.Join(scope, "+")) + } + + if state != "" { + values.Set("state", state) + } + + url.RawQuery = values.Encode() + + return url.String(), nil +} diff --git a/auth_test.go b/auth_test.go new file mode 100644 index 0000000..2b8589f --- /dev/null +++ b/auth_test.go @@ -0,0 +1,75 @@ +package kekskurseauth + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNewAuthWithConfig(t *testing.T) { + config := AuthConfig{} + config.TokenEndpoint = "http://localhost/something" + client, err := NewAuthWithConfig(config, "abc", "def") + assert.Equal(t, nil, err, "should return no error while creating Auth") + assert.Equal(t, "http://localhost/something", client.config.TokenEndpoint, "should have currect config") + assert.Equal(t, "abc", client.clientID, "should have stored currect clientid") + assert.Equal(t, "def", client.clientSecret, "should have stored currect client secret") +} + +func TestNewAuthWithConfigurationURL(t *testing.T) { + client, err := NewAuthWithConfigurationURL("http://localhost:8084/openid-configuration", "abc", "def") + assert.Nil(t, err, "should create client without any error") + assert.Equal(t, "https://auth.keks.cloud/application/o/token/", client.config.TokenEndpoint, "token endpoint should match") + assert.Equal(t, "abc", client.clientID, "should have stored currect clientid") + assert.Equal(t, "def", client.clientSecret, "should have stored currect client secret") +} + +func TestGetAuthorizationUrl(t *testing.T) { + tts := []struct { + name string + config AuthConfig + redirectURL string + scops []string + state string + exptUrl string + exptError error + }{ + { + name: "error-config-has-no-url", + exptError: ErrCantGetAuthorizationURL, + }, + { + name: "plain-url", + config: AuthConfig{AuthorizationEndpoint: "http://localhost/something"}, + exptUrl: "http://localhost/something?client_id=abc", + }, + { + name: "url-with-redirect-and-state", + config: AuthConfig{AuthorizationEndpoint: "http://localhost/something"}, + exptUrl: "http://localhost/something?client_id=abc&redirect_uri=https%3A%2F%2Fexample.com&state=randomStateStringWith%C3%A4and%C3%B6ok", + redirectURL: "https://example.com", + state: "randomStateStringWithäandöok", + }, + { + name: "url-with-scopes", + config: AuthConfig{AuthorizationEndpoint: "http://localhost/something"}, + scops: []string{"some", "söäüöäüßcopes"}, + exptUrl: "http://localhost/something?client_id=abc&scope=some%2Bs%C3%B6%C3%A4%C3%BC%C3%B6%C3%A4%C3%BC%C3%9Fcopes", + }, + } + + for _, tt := range tts { + t.Run(tt.name, func(t *testing.T) { + client, err := NewAuthWithConfig(tt.config, "abc", "def") + assert.Nil(t, err, "should be able to create client without error") + + url, err := client.GetAuthorizationURL(tt.redirectURL, tt.scops, tt.state) + if tt.exptError == nil { + assert.Nil(t, err, "should get link without error") + } else { + assert.ErrorIs(t, err, tt.exptError, "should return right error") + } + assert.Equal(t, tt.exptUrl, url, "should return right url") + }) + } +} diff --git a/config.go b/config.go new file mode 100644 index 0000000..c97bbb7 --- /dev/null +++ b/config.go @@ -0,0 +1,7 @@ +package kekskurseauth + +type AuthConfig struct { + TokenEndpoint string `json:"token_endpoint"` + UserinfoEndpoint string `json:"userinfo_endpoint"` + AuthorizationEndpoint string `json:"authorization_endpoint"` +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..2360bff --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,7 @@ +services: + oAuthDummyServer: + image: nginx + volumes: + - ./static/openid-configuration:/usr/share/nginx/html/openid-configuration + ports: + - 8084:80 diff --git a/errors.go b/errors.go new file mode 100644 index 0000000..d8ebbbc --- /dev/null +++ b/errors.go @@ -0,0 +1,8 @@ +package kekskurseauth + +import "errors" + +var ( + ErrCantGetConfiguratorData = errors.New("cant get data from configurator url") + ErrCantGetAuthorizationURL = errors.New("cant get url to recirect user to") +) diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..2a86f7a --- /dev/null +++ b/go.mod @@ -0,0 +1,11 @@ +module git.keks.cloud/kekskurse/kekskurse-auth + +go 1.23.1 + +require github.com/stretchr/testify v1.9.0 + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..60ce688 --- /dev/null +++ b/go.sum @@ -0,0 +1,10 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/static/openid-configuration b/static/openid-configuration new file mode 100644 index 0000000..5bf7c94 --- /dev/null +++ b/static/openid-configuration @@ -0,0 +1,74 @@ +{ + "issuer": "https://auth.keks.cloud/application/o/test/", + "authorization_endpoint": "https://auth.keks.cloud/application/o/authorize/", + "token_endpoint": "https://auth.keks.cloud/application/o/token/", + "userinfo_endpoint": "https://auth.keks.cloud/application/o/userinfo/", + "end_session_endpoint": "https://auth.keks.cloud/application/o/test/end-session/", + "introspection_endpoint": "https://auth.keks.cloud/application/o/introspect/", + "revocation_endpoint": "https://auth.keks.cloud/application/o/revoke/", + "device_authorization_endpoint": "https://auth.keks.cloud/application/o/device/", + "response_types_supported": [ + "code", + "id_token", + "id_token token", + "code token", + "code id_token", + "code id_token token" + ], + "response_modes_supported": [ + "query", + "fragment", + "form_post" + ], + "jwks_uri": "https://auth.keks.cloud/application/o/test/jwks/", + "grant_types_supported": [ + "authorization_code", + "refresh_token", + "implicit", + "client_credentials", + "password", + "urn:ietf:params:oauth:grant-type:device_code" + ], + "id_token_signing_alg_values_supported": [ + "RS256" + ], + "subject_types_supported": [ + "public" + ], + "token_endpoint_auth_methods_supported": [ + "client_secret_post", + "client_secret_basic" + ], + "acr_values_supported": [ + "goauthentik.io/providers/oauth2/default" + ], + "scopes_supported": [ + "profile", + "openid", + "email" + ], + "request_parameter_supported": false, + "claims_supported": [ + "sub", + "iss", + "aud", + "exp", + "iat", + "auth_time", + "acr", + "amr", + "nonce", + "email", + "email_verified", + "name", + "given_name", + "preferred_username", + "nickname", + "groups" + ], + "claims_parameter_supported": false, + "code_challenge_methods_supported": [ + "plain", + "S256" + ] +} \ No newline at end of file