Skip to content

Commit

Permalink
feat: introduce SIL internal auth server
Browse files Browse the repository at this point in the history
  • Loading branch information
Salaton committed Jan 17, 2023
1 parent 622f3fc commit 30c4c26
Show file tree
Hide file tree
Showing 6 changed files with 51 additions and 1,218 deletions.
239 changes: 38 additions & 201 deletions client.go
Original file line number Diff line number Diff line change
@@ -1,20 +1,15 @@
package authutils

import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"mime/multipart"
"net/http"
"net/url"
"strings"
"time"

"github.com/casdoor/casdoor-go-sdk/casdoorsdk"
"github.com/go-playground/validator"
"github.com/savannahghi/firebasetools"
"github.com/savannahghi/serverutils"
"golang.org/x/oauth2"
)

// Client bundles data needed by methods in order to interact with the casdoor API
Expand All @@ -25,12 +20,12 @@ type Client struct {

// Config holds the necessary authentication configurations for interacting with the casdoor service
type Config struct {
CasdoorEndpoint string `json:"endpoint"`
ClientID string `json:"client_id"`
ClientSecret string `json:"client_secret"`
CasdoorOrganization string `json:"organization"`
CasdoorApplication string `json:"application"`
Certificate string `json:"certificate"`
AuthServerEndpoint string `json:"authServerEndpoint"`
ClientID string `json:"client_id"`
ClientSecret string `json:"client_secret"`
GrantType string `json:"grant_type"`
Username string `json:"username"`
Password string `json:"password"`
}

// Validate checks if all required configuration variables are present
Expand All @@ -55,212 +50,54 @@ func NewClient(config Config) (*Client, error) {
}

client := Client{
client: &http.Client{},
client: &http.Client{
Timeout: time.Second * 60 * 30,
},
configurations: Config{
CasdoorEndpoint: config.CasdoorEndpoint,
ClientID: config.ClientID,
ClientSecret: config.ClientSecret,
CasdoorOrganization: config.CasdoorOrganization,
CasdoorApplication: config.CasdoorApplication,
AuthServerEndpoint: config.AuthServerEndpoint,
ClientID: config.ClientID,
ClientSecret: config.ClientSecret,
GrantType: config.GrantType,
Username: config.Username,
Password: config.Password,
},
}

casdoorsdk.InitConfig(
config.CasdoorEndpoint,
config.ClientID,
config.ClientSecret,
config.Certificate,
config.CasdoorOrganization,
config.CasdoorApplication,
)

return &client, nil
}

// makeRequest is a helper function for making http requests
func (c *Client) makeRequest(
ctx context.Context,
method string,
path string,
body interface{},
isAuthorized bool,
contentType string,
) (*http.Response, error) {
client := http.Client{}

encoded, err := json.Marshal(body)
if err != nil {
return nil, err
}

payload := bytes.NewBuffer(encoded)
req, err := http.NewRequestWithContext(ctx, method, path, payload)
err = client.Authenticate()
if err != nil {
return nil, err
}

if isAuthorized {
req.SetBasicAuth(c.configurations.ClientID, c.configurations.ClientSecret)
}

req.Header.Set("Accept", "application/json")
req.Header.Set("Content-Type", contentType)

response, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("an error occurred while sending a HTTP request: %w", err)
return nil, fmt.Errorf("unable to initialize server client: %w", err)
}

return response, nil
}

// Login uses the "Resource Owner Password Credentials Grant" to authenticate a user and returns an
// access token in the response
func (c *Client) Login(ctx context.Context, username, password string) (*LoginResponse, error) {
loginEndpoint := fmt.Sprintf("%s/api/login/oauth/access_token", c.configurations.CasdoorEndpoint)
payload := LoginPayload{
GrantType: "password",
ClientID: c.configurations.ClientID,
ClientSecret: c.configurations.ClientSecret,
Username: username,
Password: password,
}

response, err := c.makeRequest(ctx, http.MethodPost, loginEndpoint, payload, false, "application/json")
if err != nil {
return nil, err
}

body, err := io.ReadAll(response.Body)
if err != nil {
return nil, err
}

if response.StatusCode != http.StatusOK {
var errorResponse *CasdoorErrorResponse
err = json.Unmarshal(body, &errorResponse)
if err != nil {
return nil, err
}

return nil, fmt.Errorf("expected status code %d but got %d with error: %v", http.StatusOK, response.StatusCode, errorResponse.ErrorDescription)
}

var loginResponse *LoginResponse
err = json.Unmarshal(body, &loginResponse)
if err != nil {
return nil, err
}

return loginResponse, nil
}

// AddUser creates a user in casdoor
func (c *Client) AddUser(ctx context.Context, user *casdoorsdk.User) (bool, error) {
addUserEndpoint := fmt.Sprintf("%s/api/add-user", c.configurations.CasdoorEndpoint)
response, err := c.makeRequest(ctx, http.MethodPost, addUserEndpoint, user, true, "application/json")
if err != nil {
return false, err
}

if response.StatusCode != http.StatusOK {
return false, fmt.Errorf("casdoor: AddUser error: status code: %d", response.StatusCode)
}

body, err := io.ReadAll(response.Body)
if err != nil {
return false, err
}
var addUserResponse casdoorsdk.Response
err = json.Unmarshal(body, &addUserResponse)
if err != nil {
return false, err
}

if addUserResponse.Status != "ok" {
return false, fmt.Errorf("casdoor: failed to create user")
}

return true, nil
}

// VerifyAccessToken is used to introspect a token to determine the active state of the
// OAuth 2.0 access token and to determine meta-information about this token.
func (c *Client) VerifyAccessToken(ctx context.Context, accessToken string) (*TokenIntrospectionResponse, error) {
introspectionEndpoint := fmt.Sprintf("%s/api/login/oauth/introspect", c.configurations.CasdoorEndpoint)
formData := url.Values{}
formData.Add("token", accessToken)
formData.Add("token_type_hint", "access_token")

encodedData := formData.Encode()

resp, err := casdoorsdk.DoPostBytesRaw(introspectionEndpoint, "application/x-www-form-urlencoded", bytes.NewBufferString(encodedData))
if err != nil {
return nil, err
}

var introspectionResponse *TokenIntrospectionResponse
err = json.Unmarshal(resp, &introspectionResponse)
if err != nil {
return nil, err
}

if !introspectionResponse.Active {
return nil, fmt.Errorf("the supplied access token is invalid")
}

return introspectionResponse, nil
return &client, nil
}

// RefreshToken is used to update an access token
func (c *Client) RefreshToken(ctx context.Context, token string) (*oauth2.Token, error) {
return casdoorsdk.RefreshOAuthToken(token)
}
// Authenticate uses client credentials to log in to a slade360 authentication server
func (c *Client) Authenticate() error {
apiTokenURL := fmt.Sprintf("%s/oauth2/token/", c.configurations.AuthServerEndpoint)
credentials := url.Values{}
credentials.Set("client_id", c.configurations.ClientID)
credentials.Set("client_secret", c.configurations.ClientSecret)
credentials.Set("grant_type", c.configurations.GrantType)
credentials.Set("username", c.configurations.Username)
credentials.Set("password", c.configurations.Password)

// hasValidCasdoorBearerToken returns true with no errors if the request has a valid bearer token in the authorization header.
// Otherwise, it returns false and the error in a map with the key "error"
func (c *Client) hasValidCasdoorBearerToken(ctx context.Context, r *http.Request) (bool, map[string]string, *TokenIntrospectionResponse) {
bearerToken, err := firebasetools.ExtractBearerToken(r)
if err != nil {
// this error here will only be returned to the user if all the verification functions in the chain fail
return false, serverutils.ErrorMap(err), nil
}
encodedCredentials := strings.NewReader(credentials.Encode())

validToken, err := c.VerifyAccessToken(ctx, bearerToken)
response, err := c.client.Post(apiTokenURL, "application/x-www-form-urlencoded", encodedCredentials)
if err != nil {
return false, serverutils.ErrorMap(err), nil
return err
}

return true, nil, validToken
}

// SetUserPassword assigns a user a new password
func (c *Client) SetUserPassword(ctx context.Context, userName, oldPassword, newPassword string) (*Response, error) {
setUserPasswordEndpoint := fmt.Sprintf("%s/api/set-password", c.configurations.CasdoorEndpoint)
payload := &bytes.Buffer{}
writer := multipart.NewWriter(payload)
_ = writer.WriteField("userOwner", c.configurations.CasdoorOrganization)
_ = writer.WriteField("userName", userName)
_ = writer.WriteField("oldPassword", oldPassword)
_ = writer.WriteField("newPassword", newPassword)
err := writer.Close()
data, err := io.ReadAll(response.Body)
if err != nil {
fmt.Println(err)
return nil, nil
return err
}

resp, err := casdoorsdk.DoPostBytesRaw(setUserPasswordEndpoint, writer.FormDataContentType(), payload)
var responseData LoginResponse
err = json.Unmarshal(data, &responseData)
if err != nil {
return nil, err
return err
}

var EndpointResponse *Response
err = json.Unmarshal(resp, &EndpointResponse)
if err != nil {
return nil, err
}

return EndpointResponse, nil
return nil
}

// TODO: add update user, get user methods
62 changes: 2 additions & 60 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,72 +2,14 @@ module github.com/savannahghi/authutils

go 1.18

require (
github.com/casdoor/casdoor-go-sdk v0.17.0
github.com/go-playground/validator v9.31.0+incompatible
github.com/savannahghi/firebasetools v0.0.19
github.com/savannahghi/serverutils v0.0.7
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b
)
require github.com/go-playground/validator v9.31.0+incompatible

require (
cloud.google.com/go v0.100.2 // indirect
cloud.google.com/go/compute v1.5.0 // indirect
cloud.google.com/go/container v1.2.0 // indirect
cloud.google.com/go/errorreporting v0.2.0 // indirect
cloud.google.com/go/firestore v1.6.1 // indirect
cloud.google.com/go/iam v0.3.0 // indirect
cloud.google.com/go/logging v1.4.2 // indirect
cloud.google.com/go/monitoring v1.4.0 // indirect
cloud.google.com/go/profiler v0.2.0 // indirect
cloud.google.com/go/storage v1.18.2 // indirect
cloud.google.com/go/trace v1.2.0 // indirect
contrib.go.opencensus.io/exporter/stackdriver v0.13.6 // indirect
firebase.google.com/go v3.13.0+incompatible // indirect
github.com/99designs/gqlgen v0.13.0 // indirect
github.com/agnivade/levenshtein v1.0.3 // indirect
github.com/aws/aws-sdk-go v1.37.0 // indirect
github.com/census-instrumentation/opencensus-proto v0.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/getsentry/sentry-go v0.11.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.0 // indirect
github.com/gofrs/uuid v4.2.0+incompatible // indirect
github.com/golang-jwt/jwt/v4 v4.1.0 // indirect
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/go-cmp v0.5.7 // indirect
github.com/google/pprof v0.0.0-20220113144219-d25a53d42d00 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/googleapis/gax-go/v2 v2.1.1 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/leodido/go-urn v1.2.1 // indirect
github.com/lithammer/shortuuid v3.0.0+incompatible // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/savannahghi/enumutils v0.0.3 // indirect
github.com/savannahghi/errorcodeutil v0.0.5 // indirect
github.com/sirupsen/logrus v1.9.0 // indirect
github.com/stretchr/testify v1.7.0 // indirect
github.com/vektah/gqlparser/v2 v2.1.0 // indirect
github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect
go.opencensus.io v0.23.0 // indirect
go.opentelemetry.io/otel v1.0.0-RC1 // indirect
go.opentelemetry.io/otel/exporters/jaeger v1.0.0-RC1 // indirect
go.opentelemetry.io/otel/sdk v1.0.0-RC1 // indirect
go.opentelemetry.io/otel/trace v1.0.0-RC1 // indirect
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
golang.org/x/net v0.0.0-20220722155237-a158d28d115b // indirect
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 // indirect
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect
golang.org/x/text v0.3.8 // indirect
golang.org/x/tools v0.1.12 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
google.golang.org/api v0.71.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8 // indirect
google.golang.org/grpc v1.44.0 // indirect
google.golang.org/protobuf v1.27.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/go-playground/assert.v1 v1.2.1 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
)
Loading

0 comments on commit 30c4c26

Please sign in to comment.