Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin' into CT-2208-rand
Browse files Browse the repository at this point in the history
  • Loading branch information
bsoniam committed Jan 27, 2020
2 parents de85b57 + 665861e commit 481530f
Show file tree
Hide file tree
Showing 8 changed files with 50 additions and 79 deletions.
1 change: 1 addition & 0 deletions README.md
Expand Up @@ -70,6 +70,7 @@ ENV Variable | Parameter
CT_BRIDGE_REGISTER_USERNAME | register-techuser-username
CT_BRIDGE_REGISTER_PASSWORD | register-techuser-password
CT_BRIDGE_REGISTER_CLIENT_ID | register-techuser-client-id
CT_BRIDGE_RECAPTCHA_SECRET | recaptcha-secret
CT_BRIDGE_DB_AUDIT_RW_USERNAME | db-audit-rw-username
CT_BRIDGE_DB_AUDIT_RW_PASSWORD | db-audit-rw-password
CT_BRIDGE_DB_AUDIT_RO_USERNAME | db-audit-ro-username
Expand Down
2 changes: 1 addition & 1 deletion api/register/api.go
Expand Up @@ -60,7 +60,7 @@ const (

var (
allowedGender = map[string]bool{"M": true, "F": true}
allowedDocumentType = map[string]bool{"Identity card": true, "Passport": true}
allowedDocumentType = map[string]bool{"ID_CARD": true, "PASSPORT": true, "RESIDENCE_PERMIT": true}
)

// UserFromJSON creates a User using its json representation
Expand Down
2 changes: 1 addition & 1 deletion api/register/api_test.go
Expand Up @@ -16,7 +16,7 @@ func createValidUser() User {
phoneNumber = "00 33 686 550011"
birthDate = "29.02.2020"
birthLocation = "Bermuda"
idDocType = "Passport"
idDocType = "PASSPORT"
idDocNumber = "123456789"
idDocExpiration = "23.02.2039"
)
Expand Down
2 changes: 2 additions & 0 deletions api/register/swagger-api_register.yaml
Expand Up @@ -53,10 +53,12 @@ components:
type: string
birthDate:
type: string
description: format is DD.MM.YYYY
birthLocation:
type: string
idDocumentType:
type: string
enum: [ID_CARD, PASSPORT, RESIDENCE_PERMIT]
idDocumentNumber:
type: string
idDocumentExpiration:
Expand Down
15 changes: 12 additions & 3 deletions cmd/keycloakb/keycloak_bridge.go
Expand Up @@ -165,6 +165,7 @@ func main() {
registerPassword = c.GetString("register-techuser-password")
registerClientID = c.GetString("register-techuser-client-id")
recaptchaURL = c.GetString("recaptcha-url")
recaptchaSecret = c.GetString("recaptcha-secret")
)

// Unique ID generator
Expand Down Expand Up @@ -226,6 +227,12 @@ func main() {
}
}

// Recaptcha secret
if registerEnabled && recaptchaSecret == "" {
logger.Error(ctx, "msg", "Recaptcha secret is not configured")
return
}

// Keycloak adaptor for common-service library
commonKcAdaptor := keycloakb.NewKeycloakAuthClient(keycloakClient, logger)

Expand Down Expand Up @@ -859,7 +866,7 @@ func main() {
route.Handle("/health/check", healthChecker.MakeHandler())

// Register
var registerUserHandler = configureRegisterHandler(keycloakb.ComponentName, ComponentID, idGenerator, keycloakClient, recaptchaURL, tracer, logger)(registerEndpoints.RegisterUser)
var registerUserHandler = configureRegisterHandler(keycloakb.ComponentName, ComponentID, idGenerator, keycloakClient, recaptchaURL, recaptchaSecret, tracer, logger)(registerEndpoints.RegisterUser)

route.Path("/register/user").Methods("POST").Handler(registerUserHandler)

Expand Down Expand Up @@ -998,6 +1005,7 @@ func config(ctx context.Context, logger log.Logger) *viper.Viper {
v.SetDefault("register-techuser-password", "")
v.SetDefault("register-techuser-client-id", "")
v.SetDefault("recaptcha-url", "https://www.google.com/recaptcha/api/siteverify")
v.SetDefault("recaptcha-secret", "")

// First level of override.
pflag.String("config-file", v.GetString("config-file"), "The configuration file path can be relative or absolute.")
Expand All @@ -1012,6 +1020,7 @@ func config(ctx context.Context, logger log.Logger) *viper.Viper {

v.BindEnv("register-techuser-username", "CT_BRIDGE_REGISTER_USERNAME")
v.BindEnv("register-techuser-password", "CT_BRIDGE_REGISTER_PASSWORD")
v.BindEnv("recaptcha-secret", "CT_BRIDGE_RECAPTCHA_SECRET")

v.BindEnv("influx-username", "CT_BRIDGE_INFLUX_USERNAME")
v.BindEnv("influx-password", "CT_BRIDGE_INFLUX_PASSWORD")
Expand Down Expand Up @@ -1099,12 +1108,12 @@ func configureAccountHandler(ComponentName string, ComponentID string, idGenerat
}
}

func configureRegisterHandler(ComponentName string, ComponentID string, idGenerator idgenerator.IDGenerator, keycloakClient *keycloak.Client, recaptchaURL string, tracer tracing.OpentracingClient, logger log.Logger) func(endpoint endpoint.Endpoint) http.Handler {
func configureRegisterHandler(ComponentName string, ComponentID string, idGenerator idgenerator.IDGenerator, keycloakClient *keycloak.Client, recaptchaURL, recaptchaSecret string, tracer tracing.OpentracingClient, logger log.Logger) func(endpoint endpoint.Endpoint) http.Handler {
return func(endpoint endpoint.Endpoint) http.Handler {
var handler http.Handler
handler = register.MakeRegisterHandler(endpoint, logger)
handler = middleware.MakeHTTPCorrelationIDMW(idGenerator, tracer, logger, ComponentName, ComponentID)(handler)
handler = register.MakeHTTPRecaptchaValidationMW(recaptchaURL, logger)(handler)
handler = register.MakeHTTPRecaptchaValidationMW(recaptchaURL, recaptchaSecret, logger)(handler)
return handler
}
}
Expand Down
3 changes: 2 additions & 1 deletion configs/keycloak_bridge.yml
Expand Up @@ -165,4 +165,5 @@ register-realm: trustid
register-techuser-username: technicaluser
register-techuser-password: technicalsuperpasswordverylongandstrong
register-techuser-client-id: admin-cli
recaptcha-url: https://www.google.com/recaptcha/api/siteverify
recaptcha-url: https://www.google.com/recaptcha/api/siteverify
recaptcha-secret: theverymysterioussecretfortherecaptchaverifyoperation
35 changes: 7 additions & 28 deletions pkg/register/authorization.go
Expand Up @@ -3,7 +3,6 @@ package register
import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
Expand All @@ -18,8 +17,7 @@ import (
)

const (
regexpBasicAuth = `^[Bb]asic (.+)$`
regExpRecaptcha = `^([\w\d]+):secret=(.+),token=(.+)$`
regexpRecaptchaToken = `^[\w\d-]+$`
)

type recaptchaResponse struct {
Expand All @@ -30,44 +28,25 @@ type recaptchaResponse struct {
}

// MakeHTTPRecaptchaValidationMW retrieves the recaptcha code and checks its validity
func MakeHTTPRecaptchaValidationMW(recaptchaURL string, logger log.Logger) func(http.Handler) http.Handler {
func MakeHTTPRecaptchaValidationMW(recaptchaURL string, recaptchaSecret string, logger log.Logger) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
var authorizationHeader = req.Header.Get("Authorization")
var recaptchaToken = req.Header.Get("Authorization")
var ctx = context.TODO()

if authorizationHeader == "" {
if recaptchaToken == "" {
logger.Info(ctx, "Authorization Error", "Missing Authorization header")
httpErrorHandler(ctx, http.StatusForbidden, errors.New(errorhandler.MsgErrMissingParam+"."+errorhandler.AuthHeader), w)
return
}

var r = regexp.MustCompile(regexpBasicAuth)
var match = r.FindStringSubmatch(authorizationHeader)
if match == nil {
logger.Info(ctx, "Authorization Error", "Missing basic token")
if match, _ := regexp.MatchString(regexpRecaptchaToken, recaptchaToken); !match {
logger.Info(ctx, "Authorization Error", "Invalid recaptcha token")
httpErrorHandler(ctx, http.StatusForbidden, errors.New(errorhandler.MsgErrMissingParam+"."+errorhandler.BasicToken), w)
return
}

// Decode base 64 (RegExp matched: we got exactly 2 values. match[0] is the global matched string, match[1] is the first group)
decodedToken, err := base64.StdEncoding.DecodeString(match[1])
if err != nil {
logger.Info(ctx, "Authorization Error", "Invalid base64 token")
httpErrorHandler(ctx, http.StatusForbidden, errors.New(errorhandler.MsgErrInvalidParam+"."+errorhandler.Token), w)
return
}

// Extract username & password values
r = regexp.MustCompile(regExpRecaptcha)
match = r.FindStringSubmatch(string(decodedToken))
if match == nil {
logger.Info(ctx, "Authorization Error", "Invalid token format (recaptcha:secret={secret},token={token})")
httpErrorHandler(ctx, http.StatusForbidden, errors.New(errorhandler.MsgErrInvalidParam+"."+errorhandler.Token), w)
return
}

if !checkRecaptcha(ctx, recaptchaURL, match[2], match[3], logger) {
if !checkRecaptcha(ctx, recaptchaURL, recaptchaSecret, recaptchaToken, logger) {
httpErrorHandler(ctx, http.StatusForbidden, errors.New(errorhandler.MsgErrInvalidParam+"."+errorhandler.Token), w)
return
}
Expand Down
69 changes: 24 additions & 45 deletions pkg/register/authorization_test.go
Expand Up @@ -2,7 +2,6 @@ package register

import (
"context"
"encoding/base64"
"errors"
"net/http"
"net/http/httptest"
Expand All @@ -25,83 +24,63 @@ func TestMakeHTTPRecaptchaValidationMW(t *testing.T) {
var mockResponseWriter = mock.NewResponseWriter(mockCtrl)

var recaptchaPath = "/recaptcha"
var recaptchaSecret = "thesecretfortherecaptchaverifyprocess"
r := mux.NewRouter()
r.Handle(recaptchaPath, mockRecaptchaHandler)

ts := httptest.NewServer(r)
defer ts.Close()

var authHandler = MakeHTTPRecaptchaValidationMW(ts.URL+recaptchaPath, logger.NewNopLogger())(mockHTTPHandler)
var authHandler = MakeHTTPRecaptchaValidationMW(ts.URL+recaptchaPath, recaptchaSecret, logger.NewNopLogger())(mockHTTPHandler)
var req = http.Request{
Header: make(http.Header),
}

t.Run("Missing Basic authentication", func(t *testing.T) {
mockResponseWriter.EXPECT().WriteHeader(403)
mockResponseWriter.EXPECT().Header().Return(req.Header)
mockResponseWriter.EXPECT().Write(gomock.Any())
t.Run("Missing authentication", func(t *testing.T) {
mockResponseWriter.EXPECT().WriteHeader(http.StatusForbidden).Times(1)
mockResponseWriter.EXPECT().Header().Return(req.Header).Times(1)
mockResponseWriter.EXPECT().Write(gomock.Any()).Times(1)
authHandler.ServeHTTP(mockResponseWriter, &req)
})

t.Run("Not a valid Basic authentication", func(t *testing.T) {
req.Header.Set("Authorization", "Dont match regexp")
mockResponseWriter.EXPECT().WriteHeader(403)
mockResponseWriter.EXPECT().Header().Return(req.Header)
mockResponseWriter.EXPECT().Write(gomock.Any())
authHandler.ServeHTTP(mockResponseWriter, &req)
})

t.Run("Basic authentication is not a base 64 value", func(t *testing.T) {
var invalidBase64 = "AB"
req.Header.Set("Authorization", "Basic "+invalidBase64)
mockResponseWriter.EXPECT().WriteHeader(403)
mockResponseWriter.EXPECT().Header().Return(req.Header)
mockResponseWriter.EXPECT().Write(gomock.Any())
authHandler.ServeHTTP(mockResponseWriter, &req)
})

t.Run("Basic authentication decoded value is not like 'type:secret=qwerty,token=abcdef-789www'", func(t *testing.T) {
var basicAuthenticationValue = base64.StdEncoding.EncodeToString([]byte("admin=password"))
req.Header.Set("Authorization", "Basic "+basicAuthenticationValue)
mockResponseWriter.EXPECT().WriteHeader(403)
mockResponseWriter.EXPECT().Header().Return(req.Header)
mockResponseWriter.EXPECT().Write(gomock.Any())
t.Run("Not a valid token", func(t *testing.T) {
req.Header.Set("Authorization", "Don't match regexp")
mockResponseWriter.EXPECT().WriteHeader(http.StatusForbidden).Times(1)
mockResponseWriter.EXPECT().Header().Return(req.Header).Times(1)
mockResponseWriter.EXPECT().Write(gomock.Any()).Times(1)
authHandler.ServeHTTP(mockResponseWriter, &req)
})

t.Run("Recaptcha bad HTTP status", func(t *testing.T) {
var basicAuthenticationValue = base64.StdEncoding.EncodeToString([]byte("recaptcha:secret=abcdef,token=123456"))
req.Header.Set("Authorization", "Basic "+basicAuthenticationValue)
req.Header.Set("Authorization", recaptchaSecret)
mockRecaptchaHandler.EXPECT().ServeHTTP(gomock.Any(), gomock.Any()).Do(func(w http.ResponseWriter, req *http.Request) {
w.WriteHeader(400)
})
mockResponseWriter.EXPECT().WriteHeader(403)
mockResponseWriter.EXPECT().Header().Return(req.Header)
mockResponseWriter.EXPECT().Write(gomock.Any())
}).Times(1)
mockResponseWriter.EXPECT().WriteHeader(http.StatusForbidden).Times(1)
mockResponseWriter.EXPECT().Header().Return(req.Header).Times(1)
mockResponseWriter.EXPECT().Write(gomock.Any()).Times(1)
authHandler.ServeHTTP(mockResponseWriter, &req)
})

t.Run("Invalid recaptcha code", func(t *testing.T) {
var basicAuthenticationValue = base64.StdEncoding.EncodeToString([]byte("recaptcha:secret=abcdef,token=123456"))
req.Header.Set("Authorization", "Basic "+basicAuthenticationValue)
req.Header.Set("Authorization", recaptchaSecret)
mockRecaptchaHandler.EXPECT().ServeHTTP(gomock.Any(), gomock.Any()).Do(func(w http.ResponseWriter, req *http.Request) {
w.WriteHeader(200)
w.Write([]byte(`{"success":false}`))
})
mockResponseWriter.EXPECT().WriteHeader(403)
mockResponseWriter.EXPECT().Header().Return(req.Header)
mockResponseWriter.EXPECT().Write(gomock.Any())
}).Times(1)
mockResponseWriter.EXPECT().WriteHeader(403).Times(1)
mockResponseWriter.EXPECT().Header().Return(req.Header).Times(1)
mockResponseWriter.EXPECT().Write(gomock.Any()).Times(1)
authHandler.ServeHTTP(mockResponseWriter, &req)
})

t.Run("Recaptcha code is valid", func(t *testing.T) {
var basicAuthenticationValue = base64.StdEncoding.EncodeToString([]byte("recaptcha:secret=abcdef,token=abcdef"))
req.Header.Set("Authorization", "Basic "+basicAuthenticationValue)
req.Header.Set("Authorization", recaptchaSecret)
mockRecaptchaHandler.EXPECT().ServeHTTP(gomock.Any(), gomock.Any()).Do(func(w http.ResponseWriter, req *http.Request) {
w.WriteHeader(200)
w.Write([]byte(`{"success":true}`))
})
mockHTTPHandler.EXPECT().ServeHTTP(gomock.Any(), gomock.Any())
}).Times(1)
mockHTTPHandler.EXPECT().ServeHTTP(gomock.Any(), gomock.Any()).Times(1)
authHandler.ServeHTTP(mockResponseWriter, &req)
})
}
Expand Down

0 comments on commit 481530f

Please sign in to comment.