Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Integrate tenant authentication #92

Merged
merged 4 commits into from
Jan 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 6 additions & 1 deletion containers/docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ services:
init: true
depends_on:
- trtl
- quarterdeck
ports:
- 8080:8080
environment:
Expand All @@ -45,6 +46,10 @@ services:
- TENANT_LOG_LEVEL=info
- TENANT_CONSOLE_LOG=true
- TENANT_ALLOW_ORIGINS=http://localhost:3000
- TENANT_AUTH_KEYS_URL=http://quarterdeck:8088/.well-known/jwks.json
- TENANT_AUTH_AUDIENCE=http://localhost:3000
- TENANT_AUTH_ISSUER=http://localhost:8088
- TENANT_AUTH_COOKIE_DOMAIN=localhost
- TENANT_DATABASE_URL=trtl://trtl:4436
- TENANT_DATABASE_INSECURE=true
- TENANT_SENDGRID_API_KEY
Expand Down Expand Up @@ -142,4 +147,4 @@ services:
ports:
- 9080:3000
volumes:
- ./monitor/grafana:/var/lib/grafana
- ./monitor/grafana:/var/lib/grafana
57 changes: 56 additions & 1 deletion pkg/tenant/api/v1/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ func New(endpoint string, opts ...ClientOption) (_ TenantClient, err error) {
type APIv1 struct {
endpoint *url.URL
client *http.Client
creds string
}

// Ensures the APIv1 implements the TenantClient interface
Expand Down Expand Up @@ -795,7 +796,10 @@ func (s *APIv1) NewRequest(ctx context.Context, method, path string, data interf
req.Header.Add("Accept-Encoding", acceptEncode)
req.Header.Add("Content-Type", contentType)

// TODO: add authentication if it is available
// Add authentication if it is available
if s.creds != "" {
req.Header.Add("Authorization", "Bearer "+s.creds)
}

// Adds CSRF protection if it is available
if s.client.Jar != nil {
Expand Down Expand Up @@ -846,3 +850,54 @@ func (s *APIv1) Do(req *http.Request, data interface{}, checkStatus bool) (rep *

return rep, nil
}

// SetCredentials is a helper function for external users to override credentials at
// runtime by directly passing in the token, which is useful for testing.
// TODO: Pass in a credentials interface instead of the token string.
func (c *APIv1) SetCredentials(token string) {
c.creds = token
}
Comment on lines +854 to +859
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good stub!


// SetCSRFProtect is a helper function to set CSRF cookies on the client. This is not
// possible in a browser because of the HttpOnly flag. This method should only be used
// for testing purposes and an error is returned if the URL is not localhost. For live
// clients - the server should set these cookies. If protect is false, then the cookies
// are removed from the client by setting the cookies to an empty slice.
func (c *APIv1) SetCSRFProtect(protect bool) error {
Comment on lines +861 to +866
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this implementation from the Admin API or is it different?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's from the BFF API

if c.client.Jar == nil {
return errors.New("client does not have a cookie jar, cannot set cookies")
}

if c.endpoint.Hostname() != "127.0.0.1" && c.endpoint.Hostname() != "localhost" {
return fmt.Errorf("csrf protect is for local testing only, cannot set cookies for %s", c.endpoint.Hostname())
}

// The URL for the cookies
u := c.endpoint.ResolveReference(&url.URL{Path: "/"})

var cookies []*http.Cookie
if protect {
cookies = []*http.Cookie{
{
Name: "csrf_token",
Value: "testingcsrftoken",
Expires: time.Now().Add(10 * time.Minute),
HttpOnly: false,
},
{
Name: "csrf_reference_token",
Value: "testingcsrftoken",
Expires: time.Now().Add(10 * time.Minute),
HttpOnly: true,
},
}
} else {
cookies = c.client.Jar.Cookies(u)
for _, cookie := range cookies {
cookie.MaxAge = -1
}
}

c.client.Jar.SetCookies(u, cookies)
return nil
}
4 changes: 4 additions & 0 deletions pkg/tenant/apikeys.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ func (s *Server) APIKeyList(c *gin.Context) {
c.JSON(http.StatusNotImplemented, "not implemented yet")
}

func (s *Server) APIKeyCreate(c *gin.Context) {
c.JSON(http.StatusNotImplemented, "not implemented yet")
}

func (s *Server) APIKeyDetail(c *gin.Context) {
c.JSON(http.StatusNotImplemented, "not implemented yet")
}
Expand Down
26 changes: 26 additions & 0 deletions pkg/tenant/auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package tenant

import (
"net/http"
"time"

"github.com/gin-gonic/gin"
middleware "github.com/rotationalio/ensign/pkg/quarterdeck/middleware"
"github.com/rotationalio/ensign/pkg/tenant/api/v1"
"github.com/rs/zerolog/log"
)

// Set the maximum age of login protection cookies.
const doubleCookiesMaxAge = time.Minute * 10

// ProtectLogin prepares the front-end for login by setting the double cookie
// tokens for CSRF protection.
func (s *Server) ProtectLogin(c *gin.Context) {
expiresAt := time.Now().Add(doubleCookiesMaxAge)
if err := middleware.SetDoubleCookieToken(c, s.conf.Auth.CookieDomain, expiresAt); err != nil {
log.Error().Err(err).Msg("could not set cookies")
c.JSON(http.StatusInternalServerError, api.ErrorResponse("could not set cookies"))
return
}
c.JSON(http.StatusOK, &api.Reply{Success: true})
}
18 changes: 18 additions & 0 deletions pkg/tenant/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,21 @@ type Config struct {
LogLevel logger.LevelDecoder `split_words:"true" default:"info"` // $TENANT_LOG_LEVEL
ConsoleLog bool `split_words:"true" default:"false"` // $TENANT_CONSOLE_LOG
AllowOrigins []string `split_words:"true" default:"http://localhost:3000"` // $TENANT_ALLOW_ORIGINS
Auth AuthConfig `split_words:"true"`
Database DatabaseConfig `split_words:"true"`
SendGrid SendGridConfig `split_words:"false"`
Sentry sentry.Config
processed bool // set when the config is properly procesesed from the environment
}

// Configures the authentication and authorization for the Tenant API.
type AuthConfig struct {
KeysURL string `split_words:"true"`
Audience string `split_words:"true"`
Issuer string `split_words:"true"`
CookieDomain string `split_words:"true"`
}

// Configures the connection to trtl for replicated data storage.
type DatabaseConfig struct {
URL string `default:"trtl://localhost:4436"`
Expand Down Expand Up @@ -102,6 +111,10 @@ func (c Config) Validate() (err error) {
return err
}

if err = c.Auth.Validate(); err != nil {
return err
}

return nil
}

Expand All @@ -117,6 +130,11 @@ func (c Config) AllowAllOrigins() bool {
return false
}

func (c AuthConfig) Validate() error {
// TODO: Validate the keys URL if provided
return nil
}

// If not insecure, the cert and pool paths are required.
func (c DatabaseConfig) Validate() (err error) {
// If in testing mode, configuration is valid
Expand Down
12 changes: 12 additions & 0 deletions pkg/tenant/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ var testEnv = map[string]string{
"TENANT_LOG_LEVEL": "error",
"TENANT_CONSOLE_LOG": "true",
"TENANT_ALLOW_ORIGINS": "http://localhost:8888,http://localhost:8080",
"TENANT_AUTH_KEYS_URL": "http://localhost:8080/.well-known/jwks.json",
"TENANT_AUTH_AUDIENCE": "audience",
"TENANT_AUTH_ISSUER": "issuer",
"TENANT_AUTH_COOKIE_DOMAIN": "localhost",
"TENANT_DATABASE_URL": "trtl://localhost:4436",
"TENANT_DATABASE_INSECURE": "true",
"TENANT_DATABASE_CERT_PATH": "path/to/certs.pem",
Expand Down Expand Up @@ -64,6 +68,10 @@ func TestConfig(t *testing.T) {
require.Equal(t, zerolog.ErrorLevel, conf.GetLogLevel())
require.True(t, conf.ConsoleLog)
require.Len(t, conf.AllowOrigins, 2)
require.Equal(t, testEnv["TENANT_AUTH_KEYS_URL"], conf.Auth.KeysURL)
require.Equal(t, testEnv["TENANT_AUTH_AUDIENCE"], conf.Auth.Audience)
require.Equal(t, testEnv["TENANT_AUTH_ISSUER"], conf.Auth.Issuer)
require.Equal(t, testEnv["TENANT_AUTH_COOKIE_DOMAIN"], conf.Auth.CookieDomain)
require.Equal(t, testEnv["TENANT_DATABASE_URL"], conf.Database.URL)
require.True(t, conf.Database.Insecure)
require.Equal(t, testEnv["TENANT_DATABASE_CERT_PATH"], conf.Database.CertPath)
Expand Down Expand Up @@ -148,6 +156,10 @@ func TestAllowAllOrigins(t *testing.T) {
require.True(t, conf.AllowAllOrigins(), "expected allow all origins to be true when * is set")
}

func TestAuth(t *testing.T) {
// TODO: test AuthConfig validation
}

func TestDatabase(t *testing.T) {
// TODO: test DatabaseConfig validation
}
Expand Down