Skip to content

Commit

Permalink
WIP: social logins and identity provider
Browse files Browse the repository at this point in the history
  • Loading branch information
eleftherias committed Sep 25, 2023
1 parent bdd7921 commit 3bb1394
Show file tree
Hide file tree
Showing 35 changed files with 4,060 additions and 4,650 deletions.
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,14 @@ docker-compose up -d postgres
make migrateup
```

## Start the identity provider (Keycloak)

In order to login, we rely on an identity provider that stores the usernames and passwords.

```bash
docker-compose up -d keycloak
```

## Run the application

You will need to [initialize the database](#initialize-the-database) before you can start the application. Then run the application:
Expand Down
3 changes: 0 additions & 3 deletions cmd/cli/app/apply/apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ import (
"github.com/stacklok/mediator/cmd/cli/app/group"
"github.com/stacklok/mediator/cmd/cli/app/org"
"github.com/stacklok/mediator/cmd/cli/app/role"
"github.com/stacklok/mediator/cmd/cli/app/user"
"github.com/stacklok/mediator/internal/util"
)

Expand Down Expand Up @@ -111,8 +110,6 @@ var ApplyCmd = &cobra.Command{
org.Org_createCmd.Run(cmd, args)
} else if object.Object == "role" {
role.Role_createCmd.Run(cmd, args)
} else if object.Object == "user" {
user.User_createCmd.Run(cmd, args)
} else if object.Object == "group" {
group.Group_createCmd.Run(cmd, args)
} else {
Expand Down
152 changes: 118 additions & 34 deletions cmd/cli/app/auth/auth_login.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,43 @@
package auth

import (
"context"
"crypto/tls"
"fmt"
"net/http"
"os"
"time"

"github.com/google/uuid"
"github.com/gorilla/securecookie"
"github.com/pkg/browser"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/zitadel/oidc/v2/pkg/client/rp"
httphelper "github.com/zitadel/oidc/v2/pkg/http"
"github.com/zitadel/oidc/v2/pkg/oidc"
"golang.org/x/oauth2"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"

"github.com/stacklok/mediator/internal/config"
"github.com/stacklok/mediator/internal/util"
pb "github.com/stacklok/mediator/pkg/generated/protobuf/go/mediator/v1"
)

func userRegistered(ctx context.Context, client pb.UserServiceClient) (bool, error) {
_, err := client.GetUser(ctx, &pb.GetUserRequest{})
if err != nil {
if st, ok := status.FromError(err); ok {
if st.Code() == codes.NotFound {
return false, nil
}
}
return false, fmt.Errorf("Error retrieving user %v", err)
}
return true, nil
}

// auth_loginCmd represents the login command
var auth_loginCmd = &cobra.Command{
Use: "login",
Expand All @@ -45,57 +71,115 @@ will be saved to $XDG_CONFIG_HOME/mediator/credentials.json`,
}
},
Run: func(cmd *cobra.Command, args []string) {
username := util.GetConfigValue("username", "username", cmd, "").(string)
password := util.GetConfigValue("password", "password", cmd, "").(string)
ctx := context.Background()
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
sslcli := &http.Client{Transport: tr}
ctx = context.WithValue(ctx, oauth2.HTTPClient, sslcli)
cfg, err := config.ReadConfigFromViper(viper.GetViper())
util.ExitNicelyOnError(err, "unable to read config")

clientID := cfg.Identity.ClientId
issuer := fmt.Sprintf("%v/realms/%v", cfg.Identity.IssuerUrl, cfg.Identity.Realm)
scopes := []string{"openid"}
callbackPath := "/auth/callback"

// create encrypted cookie handler to mitigate CSRF attacks
hashKey := securecookie.GenerateRandomKey(32)
encryptKey := securecookie.GenerateRandomKey(32)
cookieHandler := httphelper.NewCookieHandler(hashKey, encryptKey, httphelper.WithUnsecure())
options := []rp.Option{
rp.WithCookieHandler(cookieHandler),
rp.WithVerifierOpts(rp.WithIssuedAtOffset(5 * time.Second)),
rp.WithPKCE(cookieHandler),
}

conn, err := util.GrpcForCommand(cmd)
util.ExitNicelyOnError(err, "Error getting grpc connection")
defer conn.Close()
// Get random port
port, err := util.GetRandomPort()
util.ExitNicelyOnError(err, "Error getting random port")
redirectURI := fmt.Sprintf("http://localhost:%v%v", port, callbackPath)
provider, err := rp.NewRelyingPartyOIDC(issuer, clientID, "", redirectURI, scopes, options...)
util.ExitNicelyOnError(err, "error creating provider")

client := pb.NewAuthServiceClient(conn)
ctx, cancel := util.GetAppContext()
defer cancel()
state := func() string {
return uuid.New().String()
}

// call login endpoint
resp, err := client.LogIn(ctx, &pb.LogInRequest{Username: username, Password: password})
if err != nil {
ret := status.Convert(err)
fmt.Fprintf(os.Stderr, "Error logging in: Code: %d\nName: %s\nDetails: %s\n", ret.Code(), ret.Code().String(), ret.Message())
tokenChan := make(chan *oidc.Tokens[*oidc.IDTokenClaims])

callback := func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens[*oidc.IDTokenClaims], state string, rp rp.RelyingParty) {

tokenChan <- tokens
msg := "<p><strong>Authentication successful</strong>. You may now close this tab and return to your terminal.</p>"
// send a success message to the browser
fmt.Fprint(w, msg)
}
http.Handle("/login", rp.AuthURLHandler(state, provider))
http.Handle(callbackPath, rp.CodeExchangeHandler(callback, provider))

os.Exit(int(ret.Code()))
server := &http.Server{
Addr: fmt.Sprintf(":%d", port),
ReadHeaderTimeout: time.Second * 10,
}
// Start the server in a goroutine
go func() {
_ = server.ListenAndServe()
}()

// marshal the credentials to json
creds := util.Credentials{
AccessToken: resp.AccessToken,
RefreshToken: resp.RefreshToken,
AccessTokenExpiresIn: int(resp.AccessTokenExpiresIn),
RefreshTokenExpiresIn: int(resp.RefreshTokenExpiresIn),
// get the OAuth authorization URL
loginUrl := fmt.Sprintf("http://localhost:%v/login", port)

// Redirect user to provider to log in
fmt.Printf("Your browser will now be opened to: %s\n", loginUrl)
fmt.Println("Please follow the instructions on the page to log in.")

// open user's browser to login page
if err := browser.OpenURL(loginUrl); err != nil {
fmt.Printf("You may login by pasting this URL into your browser: %s\n", loginUrl)
}

fmt.Printf("Waiting for token\n")

// wait for the token to be received
token := <-tokenChan

// save credentials
filePath, err := util.SaveCredentials(creds)
filePath, err := util.SaveCredentials(token)
if err != nil {
fmt.Println(err)
}

fmt.Printf("You have been successfully logged in. Your access credentials saved to %s\n"+
"Remember that if that's your first login, you will need to update your password "+
"using the user update --password command", filePath)
conn, err := util.GrpcForCommand(cmd)
util.ExitNicelyOnError(err, "Error getting grpc connection")
defer conn.Close()
client := pb.NewUserServiceClient(conn)

// check if the user already exists in the local database
registered, err := userRegistered(ctx, client)
util.ExitNicelyOnError(err, "Error fetching user")

if !registered {
fmt.Println("First login, registering user.")
// register the user and add them to organization 1
// TODO: register the user in their own organization
_, err = client.CreateUser(ctx, &pb.CreateUserRequest{
OrganizationId: 1,
})
util.ExitNicelyOnError(err, "Error registering user")
}

fmt.Printf("You have been successfully logged in. Your access credentials saved to %s\n",
filePath)

// shut down the HTTP server
err = server.Shutdown(context.Background())
util.ExitNicelyOnError(err, "Failed to shut down server")

fmt.Println("Authentication successful")
},
}

func init() {
AuthCmd.AddCommand(auth_loginCmd)
auth_loginCmd.Flags().StringP("username", "u", "", "Username to use for authentication")
auth_loginCmd.Flags().StringP("password", "p", "", "Password to use for authentication")

if err := auth_loginCmd.MarkFlagRequired("username"); err != nil {
fmt.Fprintf(os.Stderr, "Error marking flag as required: %s\n", err)
}
if err := auth_loginCmd.MarkFlagRequired("password"); err != nil {
fmt.Fprintf(os.Stderr, "Error marking flag as required: %s\n", err)
}

}
69 changes: 32 additions & 37 deletions cmd/cli/app/auth/auth_refresh.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,10 @@
package auth

import (
"context"
"fmt"
"os"
"time"

"github.com/spf13/cobra"
"github.com/spf13/viper"

"github.com/stacklok/mediator/internal/util"
pb "github.com/stacklok/mediator/pkg/generated/protobuf/go/mediator/v1"
"os"
)

// Auth_refreshCmd represents the auth refresh command
Expand All @@ -45,36 +39,37 @@ var Auth_refreshCmd = &cobra.Command{
}
},
Run: func(cmd *cobra.Command, args []string) {
// load old credentials
oldCreds, err := util.LoadCredentials()
util.ExitNicelyOnError(err, "Error loading credentials")

conn, err := util.GrpcForCommand(cmd)
util.ExitNicelyOnError(err, "Error getting grpc connection")
defer conn.Close()

ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()

client := pb.NewAuthServiceClient(conn)
util.ExitNicelyOnError(err, "Error getting grpc connection")

resp, err := client.RefreshToken(ctx, &pb.RefreshTokenRequest{})
util.ExitNicelyOnError(err, "Error refreshing token")

// marshal the credentials to json. Only refresh access token
creds := util.Credentials{
AccessToken: resp.AccessToken,
RefreshToken: oldCreds.RefreshToken,
AccessTokenExpiresIn: int(resp.AccessTokenExpiresIn),
RefreshTokenExpiresIn: oldCreds.RefreshTokenExpiresIn,
}

// save credentials
filePath, err := util.SaveCredentials(creds)
util.ExitNicelyOnError(err, "Error saving credentials")

fmt.Printf("Credentials saved to %s\n", filePath)
// TODO: implement refresh token
//// load old credentials
//oldCreds, err := util.LoadCredentials()
//util.ExitNicelyOnError(err, "Error loading credentials")
//
//conn, err := util.GrpcForCommand(cmd)
//util.ExitNicelyOnError(err, "Error getting grpc connection")
//defer conn.Close()
//
//ctx, cancel := context.WithTimeout(context.Background(), time.Second)
//defer cancel()
//
//client := pb.NewAuthServiceClient(conn)
//util.ExitNicelyOnError(err, "Error getting grpc connection")
//
//resp, err := client.RefreshToken(ctx, &pb.RefreshTokenRequest{})
//util.ExitNicelyOnError(err, "Error refreshing token")
//
//// marshal the credentials to json. Only refresh access token
//creds := util.Credentials{
// AccessToken: resp.AccessToken,
// RefreshToken: oldCreds.RefreshToken,
// AccessTokenExpiresIn: int(resp.AccessTokenExpiresIn),
// RefreshTokenExpiresIn: oldCreds.RefreshTokenExpiresIn,
//}
//
//// save credentials
//filePath, err := util.SaveCredentials(creds)
//util.ExitNicelyOnError(err, "Error saving credentials")
//
//fmt.Printf("Credentials saved to %s\n", filePath)
},
}

Expand Down
Loading

0 comments on commit 3bb1394

Please sign in to comment.