Skip to content

Commit

Permalink
Merge pull request #1014 from eleftherias/social-login
Browse files Browse the repository at this point in the history
Identity provider login from mediator CLI
  • Loading branch information
eleftherias authored Sep 29, 2023
2 parents e8dc409 + 5d25ddc commit 38091a0
Show file tree
Hide file tree
Showing 53 changed files with 5,271 additions and 6,755 deletions.
31 changes: 15 additions & 16 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 Expand Up @@ -179,29 +187,20 @@ Before running the app, please copy the content of `config/config.yaml.example`

## Login

First, login with the default password for the database:
First, login with the default credentials:

```bash
go run ./cmd/cli/main.go auth login -u root -p P4ssw@rd
go run ./cmd/cli/main.go auth login
```

This will result in the following prompt:

```
You have been successfully logged in. Your access credentials saved to /var/home/jaosorior/.config/mediator/credentials.json
Remember that if that's your first login, you will need to update your password using the user update --password command
```
This will open a browser window with the identity provider login page.
Enter the credentials root / P4ssw@rd. You will immediately be prompted to change your password.
Upon successful authentication you can close your browser.

At this point, you should update the password:
You will see the following prompt in your terminal:

```bash
go run ./cmd/cli/main.go user update -p f00b@r123 -c f00b@r123
```

And subsequently log in again with your new password

```bash
go run ./cmd/cli/main.go auth login -u root -p 'f00b@r123'
You have been successfully logged in. Your access credentials saved to /var/home/jaosorior/.config/mediator/credentials.json
```

## Enroll provider
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
157 changes: 123 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,42 @@
package auth

import (
"context"
"fmt"
"net/http"
"net/url"
"os"
"time"

"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"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"

"github.com/stacklok/mediator/internal/config"
mcrypto "github.com/stacklok/mediator/internal/crypto"
"github.com/stacklok/mediator/internal/util"
pb "github.com/stacklok/mediator/pkg/api/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 +70,121 @@ 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()
cfg, err := config.ReadConfigFromViper(viper.GetViper())
util.ExitNicelyOnError(err, "unable to read config")

clientID := cfg.Identity.ClientId

parsedURL, err := url.Parse(cfg.Identity.IssuerUrl)
util.ExitNicelyOnError(err, "Error parsing issuer URL")
issuerUrl := parsedURL.JoinPath("realms", 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(),
httphelper.WithSameSite(http.SameSiteLaxMode))
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")

client := pb.NewAuthServiceClient(conn)
ctx, cancel := util.GetAppContext()
defer cancel()
parsedURL, err = url.Parse(fmt.Sprintf("http://localhost:%v", port))
util.ExitNicelyOnError(err, "Error creating callback server")
redirectURI := parsedURL.JoinPath(callbackPath)

// 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())
provider, err := rp.NewRelyingPartyOIDC(issuerUrl.String(), clientID, "", redirectURI.String(), scopes, options...)
util.ExitNicelyOnError(err, "error creating identity provider reference")

os.Exit(int(ret.Code()))
stateFn := func() string {
state, err := mcrypto.GenerateNonce()
util.ExitNicelyOnError(err, "error generating state for login")
return state
}

// marshal the credentials to json
creds := util.Credentials{
AccessToken: resp.AccessToken,
RefreshToken: resp.RefreshToken,
AccessTokenExpiresIn: int(resp.AccessTokenExpiresIn),
RefreshTokenExpiresIn: int(resp.RefreshTokenExpiresIn),
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 := "<div><h2>Authentication successful</h2><div>You may now close this tab and return to your terminal.</div></div>"
// send a success message to the browser
fmt.Fprint(w, msg)
}
http.Handle("/login", rp.AuthURLHandler(stateFn, provider))
http.Handle(callbackPath, rp.CodeExchangeHandler(callback, provider))

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

// 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)
}

}
36 changes: 1 addition & 35 deletions cmd/cli/app/auth/auth_refresh.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,11 @@
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/api/protobuf/go/mediator/v1"
)

// Auth_refreshCmd represents the auth refresh command
Expand All @@ -45,36 +40,7 @@ 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
},
}

Expand Down
Loading

0 comments on commit 38091a0

Please sign in to comment.