diff --git a/Gopkg.lock b/Gopkg.lock index 5bd387a..a2657da 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -10,12 +10,12 @@ revision = "065b426bd41667456c1a924468f507673629c46b" [[projects]] - digest = "1:56c130d885a4aacae1dd9c7b71cfe39912c7ebc1ff7d2b46083c8812996dc43b" - name = "github.com/davecgh/go-spew" - packages = ["spew"] + digest = "1:0c07a9cb3d3c845439a4fcaae6c8bdd0e7727cbbd3acf1e032e5d4a2dc132306" + name = "github.com/gbrlsnchs/jwt" + packages = ["."] pruneopts = "" - revision = "346938d642f2ec3594ed81d874461961cd0faa76" - version = "v1.1.0" + revision = "808efa0714baff8c25cc65ef8681966740beb9f9" + version = "v2.0.0" [[projects]] digest = "1:bcb38c8fc9b21bb8682ce2d605a7d4aeb618abc7f827e3ac0b27c0371fdb23fb" @@ -33,14 +33,6 @@ revision = "645ef00459ed84a119197bfb8d8205042c6df63d" version = "v0.8.0" -[[projects]] - digest = "1:256484dbbcd271f9ecebc6795b2df8cad4c458dd0f5fd82a8c2fa0c29f233411" - name = "github.com/pmezard/go-difflib" - packages = ["difflib"] - pruneopts = "" - revision = "792786c7400a136282c1664665ae0a8db921c6c2" - version = "v1.0.0" - [[projects]] branch = "master" digest = "1:386e12afcfd8964907c92dffd106860c0dedd71dbefae14397b77b724a13343b" @@ -60,17 +52,6 @@ revision = "e57e3eeb33f795204c1ca35f56c44f83227c6e66" version = "v1.0.0" -[[projects]] - digest = "1:a30066593578732a356dc7e5d7f78d69184ca65aeeff5939241a3ab10559bb06" - name = "github.com/stretchr/testify" - packages = [ - "assert", - "require", - ] - pruneopts = "" - revision = "12b6f73e6084dad08a7c6e575284b177ecafbc71" - version = "v1.2.1" - [[projects]] branch = "master" digest = "1:2ea6df0f542cc95a5e374e9cdd81eaa599ed0d55366eef92d2f6b9efa2795c07" @@ -185,11 +166,9 @@ analyzer-version = 1 input-imports = [ "github.com/coreos/go-oidc", - "github.com/davecgh/go-spew/spew", + "github.com/gbrlsnchs/jwt", "github.com/pkg/errors", "github.com/spf13/pflag", - "github.com/stretchr/testify/assert", - "github.com/stretchr/testify/require", "gopkg.in/h2non/gentleman.v2", "gopkg.in/h2non/gentleman.v2/plugin", "gopkg.in/h2non/gentleman.v2/plugins/body", diff --git a/Gopkg.toml b/Gopkg.toml index 4c9711f..7aa9efa 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -32,3 +32,8 @@ [[constraint]] name = "gopkg.in/h2non/gentleman.v2" version = "2.0.0" + + +[[constraint]] + name = "github.com/gbrlsnchs/jwt" + version = "2.0.0" diff --git a/authentication_management.go b/authentication_management.go index c6e6387..45a3de1 100644 --- a/authentication_management.go +++ b/authentication_management.go @@ -48,7 +48,7 @@ func (c *Client) DeleteAuthenticatorConfig(accessToken string, realmName, config } // CreateAuthenticationExecution add new authentication execution -func (c *Client) CreateAuthenticationExecution(accessToken string, realmName string, authExec AuthenticationExecutionRepresentation) error { +func (c *Client) CreateAuthenticationExecution(accessToken string, realmName string, authExec AuthenticationExecutionRepresentation) (string, error) { return c.post(accessToken, nil, url.Path(authenticationManagementPath+"/executions"), url.Param("realm", realmName), body.JSON(authExec)) } @@ -59,22 +59,26 @@ func (c *Client) DeleteAuthenticationExecution(accessToken string, realmName, ex // UpdateAuthenticationExecution update execution with new configuration. func (c *Client) UpdateAuthenticationExecution(accessToken string, realmName, executionID string, authConfig AuthenticatorConfigRepresentation) error { - return c.post(accessToken, nil, url.Path(authenticationManagementPath+"/executions/:id/config"), url.Param("realm", realmName), url.Param("id", executionID), body.JSON(authConfig)) + _, err := c.post(accessToken, nil, url.Path(authenticationManagementPath+"/executions/:id/config"), url.Param("realm", realmName), url.Param("id", executionID), body.JSON(authConfig)) + return err } // LowerExecutionPriority lowers the execution’s priority. func (c *Client) LowerExecutionPriority(accessToken string, realmName, executionID string) error { - return c.post(accessToken, nil, url.Path(authenticationManagementPath+"/executions/:id/lower-priority"), url.Param("realm", realmName), url.Param("id", executionID)) + _, err := c.post(accessToken, nil, url.Path(authenticationManagementPath+"/executions/:id/lower-priority"), url.Param("realm", realmName), url.Param("id", executionID)) + return err } // RaiseExecutionPriority raise the execution’s priority. func (c *Client) RaiseExecutionPriority(accessToken string, realmName, executionID string) error { - return c.post(accessToken, nil, url.Path(authenticationManagementPath+"/executions/:id/raise-priority"), url.Param("realm", realmName), url.Param("id", executionID)) + _, err := c.post(accessToken, nil, url.Path(authenticationManagementPath+"/executions/:id/raise-priority"), url.Param("realm", realmName), url.Param("id", executionID)) + return err } // CreateAuthenticationFlow creates a new authentication flow. func (c *Client) CreateAuthenticationFlow(accessToken string, realmName string, authFlow AuthenticationFlowRepresentation) error { - return c.post(accessToken, nil, url.Path(authenticationManagementPath+"/flows"), url.Param("realm", realmName), body.JSON(authFlow)) + _, err := c.post(accessToken, nil, url.Path(authenticationManagementPath+"/flows"), url.Param("realm", realmName), body.JSON(authFlow)) + return err } // GetAuthenticationFlows returns a list of authentication flows. @@ -89,7 +93,8 @@ func (c *Client) GetAuthenticationFlows(accessToken string, realmName string) ([ // 'newName' is the new name of the authentication flow. func (c *Client) CopyExistingAuthenticationFlow(accessToken string, realmName, flowAlias, newName string) error { var m = map[string]string{"newName": newName} - return c.post(accessToken, nil, url.Path(authenticationManagementPath+"/flows/:flowAlias/copy"), url.Param("realm", realmName), url.Param("flowAlias", flowAlias), body.JSON(m)) + _, err := c.post(accessToken, nil, url.Path(authenticationManagementPath+"/flows/:flowAlias/copy"), url.Param("realm", realmName), url.Param("flowAlias", flowAlias), body.JSON(m)) + return err } // GetAuthenticationExecutionForFlow returns the authentication executions for a flow. @@ -106,16 +111,16 @@ func (c *Client) UpdateAuthenticationExecutionForFlow(accessToken string, realmN // CreateAuthenticationExecutionForFlow add a new authentication execution to a flow. // 'flowAlias' is the alias of the parent flow. -func (c *Client) CreateAuthenticationExecutionForFlow(accessToken string, realmName, flowAlias, provider string) error { +func (c *Client) CreateAuthenticationExecutionForFlow(accessToken string, realmName, flowAlias, provider string) (string, error) { var m = map[string]string{"provider": provider} - return c.post(accessToken, url.Path(authenticationManagementPath+"/flows/:flowAlias/executions/execution"), url.Param("realm", realmName), url.Param("flowAlias", flowAlias), body.JSON(m)) + return c.post(accessToken, nil, url.Path(authenticationManagementPath+"/flows/:flowAlias/executions/execution"), url.Param("realm", realmName), url.Param("flowAlias", flowAlias), body.JSON(m)) } // CreateFlowWithExecutionForExistingFlow add a new flow with a new execution to an existing flow. // 'flowAlias' is the alias of the parent authentication flow. -func (c *Client) CreateFlowWithExecutionForExistingFlow(accessToken string, realmName, flowAlias, alias, flowType, provider, description string) error { +func (c *Client) CreateFlowWithExecutionForExistingFlow(accessToken string, realmName, flowAlias, alias, flowType, provider, description string) (string, error) { var m = map[string]string{"alias": alias, "type": flowType, "provider": provider, "description": description} - return c.post(accessToken, url.Path(authenticationManagementPath+"/flows/:flowAlias/executions/flow"), url.Param("realm", realmName), url.Param("flowAlias", flowAlias), body.JSON(m)) + return c.post(accessToken, nil, url.Path(authenticationManagementPath+"/flows/:flowAlias/executions/flow"), url.Param("realm", realmName), url.Param("flowAlias", flowAlias), body.JSON(m)) } // GetAuthenticationFlow gets the authentication flow for id. @@ -154,7 +159,8 @@ func (c *Client) GetConfigDescriptionForClients(accessToken string, realmName st // RegisterRequiredAction register a new required action. func (c *Client) RegisterRequiredAction(accessToken string, realmName, providerID, name string) error { var m = map[string]string{"providerId": providerID, "name": name} - return c.post(accessToken, url.Path(authenticationManagementPath+"/register-required-action"), url.Param("realm", realmName), body.JSON(m)) + _, err := c.post(accessToken, nil, url.Path(authenticationManagementPath+"/register-required-action"), url.Param("realm", realmName), body.JSON(m)) + return err } // GetRequiredActions returns a list of required actions. diff --git a/client_attribute_certificate.go b/client_attribute_certificate.go index 8a0317e..0cdfe4e 100644 --- a/client_attribute_certificate.go +++ b/client_attribute_certificate.go @@ -21,34 +21,34 @@ func (c *Client) GetKeyInfo(accessToken string, realmName, idClient, attr string // GetKeyStore returns a keystore file for the client, containing private key and public certificate. idClient is the id of client (not client-id). func (c *Client) GetKeyStore(accessToken string, realmName, idClient, attr string, keyStoreConfig KeyStoreConfig) ([]byte, error) { var resp = []byte{} - var err = c.post(accessToken, &resp, url.Path(clientAttrCertPath+"/download"), url.Param("realm", realmName), url.Param("id", idClient), url.Param("attr", attr), body.JSON(keyStoreConfig)) + _, err := c.post(accessToken, &resp, url.Path(clientAttrCertPath+"/download"), url.Param("realm", realmName), url.Param("id", idClient), url.Param("attr", attr), body.JSON(keyStoreConfig)) return resp, err } // GenerateCertificate generates a new certificate with new key pair. idClient is the id of client (not client-id). func (c *Client) GenerateCertificate(accessToken string, realmName, idClient, attr string) (CertificateRepresentation, error) { var resp = CertificateRepresentation{} - var err = c.post(accessToken, &resp, url.Path(clientAttrCertPath+"/generate"), url.Param("realm", realmName), url.Param("id", idClient), url.Param("attr", attr)) + _, err := c.post(accessToken, &resp, url.Path(clientAttrCertPath+"/generate"), url.Param("realm", realmName), url.Param("id", idClient), url.Param("attr", attr)) return resp, err } // GenerateKeyPairAndCertificate generates a keypair and certificate and serves the private key in a specified keystore format. func (c *Client) GenerateKeyPairAndCertificate(accessToken string, realmName, idClient, attr string, keyStoreConfig KeyStoreConfig) ([]byte, error) { var resp = []byte{} - var err = c.post(accessToken, &resp, url.Path(clientAttrCertPath+"/generate-and-download"), url.Param("realm", realmName), url.Param("id", idClient), url.Param("attr", attr), body.JSON(keyStoreConfig)) + _, err := c.post(accessToken, &resp, url.Path(clientAttrCertPath+"/generate-and-download"), url.Param("realm", realmName), url.Param("id", idClient), url.Param("attr", attr), body.JSON(keyStoreConfig)) return resp, err } // UploadCertificatePrivateKey uploads a certificate and eventually a private key. func (c *Client) UploadCertificatePrivateKey(accessToken string, realmName, idClient, attr string, file []byte) (CertificateRepresentation, error) { var resp = CertificateRepresentation{} - var err = c.post(accessToken, &resp, url.Path(clientAttrCertPath+"/upload"), url.Param("realm", realmName), url.Param("id", idClient), url.Param("attr", attr), body.Reader(bytes.NewReader(file))) + _, err := c.post(accessToken, &resp, url.Path(clientAttrCertPath+"/upload"), url.Param("realm", realmName), url.Param("id", idClient), url.Param("attr", attr), body.Reader(bytes.NewReader(file))) return resp, err } // UploadCertificate uploads only a certificate, not the private key. func (c *Client) UploadCertificate(accessToken string, realmName, idClient, attr string, file []byte) (CertificateRepresentation, error) { var resp = CertificateRepresentation{} - var err = c.post(accessToken, &resp, url.Path(clientAttrCertPath+"/upload-certificate"), url.Param("realm", realmName), url.Param("id", idClient), url.Param("attr", attr), body.Reader(bytes.NewReader(file))) + _, err := c.post(accessToken, &resp, url.Path(clientAttrCertPath+"/upload-certificate"), url.Param("realm", realmName), url.Param("id", idClient), url.Param("attr", attr), body.Reader(bytes.NewReader(file))) return resp, err } diff --git a/client_initial_access.go b/client_initial_access.go index 942d4ee..1c9a1b9 100644 --- a/client_initial_access.go +++ b/client_initial_access.go @@ -12,7 +12,7 @@ const ( // CreateClientInitialAccess creates a new initial access token. func (c *Client) CreateClientInitialAccess(accessToken string, realmName string, access ClientInitialAccessCreatePresentation) (ClientInitialAccessPresentation, error) { var resp = ClientInitialAccessPresentation{} - var err = c.post(accessToken, &resp, url.Path(clientInitialAccessPath), url.Param("realm", realmName), body.JSON(access)) + _, err := c.post(accessToken, &resp, nil, url.Path(clientInitialAccessPath), url.Param("realm", realmName), body.JSON(access)) return resp, err } diff --git a/client_role_mappings.go b/client_role_mappings.go index bf96db3..a06cadb 100644 --- a/client_role_mappings.go +++ b/client_role_mappings.go @@ -6,22 +6,30 @@ import ( ) const ( - clientRoleMappingPath = "/auth/admin/realms/:realm/groups/:id/role-mappings/clients/:client" + clientRoleMappingPath = "/auth/admin/realms/:realm/users/:id/role-mappings/clients/:client" + realmRoleMappingPath = "/auth/admin/realms/:realm/users/:id/role-mappings/realm" ) -// CreateClientsRoleMapping add client-level roles to the user role mapping. -func (c *Client) CreateClientsRoleMapping(accessToken string, realmName, groupID, clientID string, roles []RoleRepresentation) error { - return c.post(accessToken, nil, url.Path(clientRoleMappingPath), url.Param("realm", realmName), url.Param("id", groupID), url.Param("client", clientID), body.JSON(roles)) +// AddClientRoleMapping add client-level roles to the user role mapping. +func (c *Client) AddClientRolesToUserRoleMapping(accessToken string, realmName, userID, clientID string, roles []RoleRepresentation) error { + _, err := c.post(accessToken, nil, url.Path(clientRoleMappingPath), url.Param("realm", realmName), url.Param("id", userID), url.Param("client", clientID), body.JSON(roles)) + return err } -// GetClientsRoleMapping gets client-level role mappings for the user, and the app. -func (c *Client) GetClientsRoleMapping(accessToken string, realmName, groupID, clientID string) ([]RoleRepresentation, error) { +// GetClientRoleMappings gets client-level role mappings for the user, and the app. +func (c *Client) GetClientRoleMappings(accessToken string, realmName, userID, clientID string) ([]RoleRepresentation, error) { var resp = []RoleRepresentation{} - var err = c.get(accessToken, &resp, url.Path(clientRoleMappingPath), url.Param("realm", realmName), url.Param("id", groupID), url.Param("client", clientID)) + var err = c.get(accessToken, &resp, url.Path(clientRoleMappingPath), url.Param("realm", realmName), url.Param("id", userID), url.Param("client", clientID)) return resp, err } -// DeleteClientsRoleMapping deletes client-level roles from user role mapping. -func (c *Client) DeleteClientsRoleMapping(accessToken string, realmName, groupID, clientID string) error { - return c.delete(accessToken, url.Path(clientRoleMappingPath), url.Param("realm", realmName), url.Param("id", groupID), url.Param("client", clientID)) +// DeleteClientRolesFromUserRoleMapping deletes client-level roles from user role mapping. +func (c *Client) DeleteClientRolesFromUserRoleMapping(accessToken string, realmName, userID, clientID string) error { + return c.delete(accessToken, url.Path(clientRoleMappingPath), url.Param("realm", realmName), url.Param("id", userID), url.Param("client", clientID)) +} + +func (c *Client) GetRealmLevelRoleMappings(accessToken string, realmName, userID string) ([]RoleRepresentation, error) { + var resp = []RoleRepresentation{} + var err = c.get(accessToken, &resp, url.Path(realmRoleMappingPath), url.Param("realm", realmName), url.Param("id", userID)) + return resp, err } diff --git a/definitions.go b/definitions.go index f99d909..1577444 100644 --- a/definitions.go +++ b/definitions.go @@ -626,10 +626,10 @@ type UserFederationProviderRepresentation struct { } type UserRepresentation struct { - Access *map[string]interface{} `json:"access,omitempty"` - Attributes *map[string]interface{} `json:"attributes,omitempty"` + Access *map[string]bool `json:"access,omitempty"` + Attributes *map[string][]string `json:"attributes,omitempty"` ClientConsents *[]UserConsentRepresentation `json:"clientConsents,omitempty"` - ClientRoles *map[string]interface{} `json:"clientRoles,omitempty"` + ClientRoles *map[string][]string `json:"clientRoles,omitempty"` CreatedTimestamp *int64 `json:"createdTimestamp,omitempty"` Credentials *[]CredentialRepresentation `json:"credentials,omitempty"` DisableableCredentialTypes *[]string `json:"disableableCredentialTypes,omitempty"` diff --git a/integration/integration.go b/integration/integration.go index 81c4cb5..8a9a8db 100644 --- a/integration/integration.go +++ b/integration/integration.go @@ -15,6 +15,19 @@ const ( user = "version" ) +// This should be oncverted into +// GetClient(accessToken string, realmName, idClient string) (kc.ClientRepresentation, error) +// GetClientRoleMappings(accessToken string, realmName, userID, clientID string) ([]kc.RoleRepresentation, error) +// AddClientRolesToUserRoleMapping(accessToken string, realmName, userID, clientID string, roles []kc.RoleRepresentation) error +// GetRealmLevelRoleMappings(accessToken string, realmName, userID string) ([]kc.RoleRepresentation, error) +// ResetPassword(accessToken string, realmName string, userID string) error +// SendVerifyEmail(accessToken string, realmName string, userID string) error + +// GetRoles(accessToken string, realmName string) ([]kc.RoleRepresentation, error) +// GetRole(accessToken string, realmName string, roleID string) (kc.RoleRepresentation, error) +// GetClientRoles(accessToken string, realmName, idClient string) ([]kc.RoleRepresentation, error) +// CreateClientRole(accessToken string, realmName, clientID string, role kc.RoleRepresentation) (string, error) + func main() { var conf = getKeycloakConfig() var client, err = keycloak.New(*conf) @@ -28,6 +41,11 @@ func main() { log.Fatalf("could not get access token: %v", err) } + err = client.VerifyToken("master", accessToken) + if err != nil { + log.Fatalf("could not validate access token: %v", err) + } + // Delete test realm client.DeleteRealm(accessToken, tstRealm) @@ -49,7 +67,8 @@ func main() { // Create test realm. { var realm = tstRealm - var err = client.CreateRealm(accessToken, keycloak.RealmRepresentation{ + var err error + _, err = client.CreateRealm(accessToken, keycloak.RealmRepresentation{ Realm: &realm, }) if err != nil { @@ -111,7 +130,8 @@ func main() { for _, u := range tstUsers { var username = strings.ToLower(u.firstname + "." + u.lastname) var email = username + "@cloudtrust.ch" - var err = client.CreateUser(accessToken, tstRealm, keycloak.UserRepresentation{ + var err error + _, err = client.CreateUser(accessToken, tstRealm, keycloak.UserRepresentation{ Username: &username, FirstName: &u.firstname, LastName: &u.lastname, @@ -120,6 +140,7 @@ func main() { if err != nil { log.Fatalf("could not create test users: %v", err) } + } // Check that all users where created. { @@ -145,6 +166,17 @@ func main() { if len(users) != 50 { log.Fatalf("there should be 50 users") } + + user, err := client.GetUser(accessToken, tstRealm, *(users[0].Id)) + if err != nil { + log.Fatalf("could not get user") + } + + if !(*(user.Username) != "") { + log.Fatalf("Username should not be empty") + } + + fmt.Println("Test user retrieved.") } { // email. @@ -207,6 +239,7 @@ func main() { log.Fatalf("there should be 7 users matched by search") } } + fmt.Println("Test users retrieved.") } @@ -315,34 +348,15 @@ func main() { } } -/* -// GetUser get the represention of the user. -func (c *Client) GetUser(realmName, userID string) (UserRepresentation, error) { - var resp = UserRepresentation{} - var err = c.get(&resp, url.Path(userIDPath), url.Param("realm", realmName), url.Param("id", userID)) - return resp, err -} - -// UpdateUser update the user. -func (c *Client) UpdateUser(realmName, userID string, user UserRepresentation) error { - return c.put(url.Path(userIDPath), url.Param("realm", realmName), url.Param("id", userID), body.JSON(user)) -} - -// DeleteUser deletes the user. -func (c *Client) DeleteUser(realmName, userID string) error { - return c.delete(url.Path(userIDPath), url.Param("realm", realmName), url.Param("id", userID)) -} - - -*/ - func getKeycloakConfig() *keycloak.Config { - var adr = pflag.String("url", "http://localhost:8080", "keycloak address") + var apiAddr = pflag.String("urlKc", "http://localhost:8080", "keycloak address") + var tokenAddr = pflag.String("url", "http://localhost:8080", "token address") pflag.Parse() return &keycloak.Config{ - Addr: *adr, - Timeout: 10 * time.Second, + AddrTokenProvider: *tokenAddr, + AddrAPI: *apiAddr, + Timeout: 10 * time.Second, } } diff --git a/keycloak_client.go b/keycloak_client.go index b367dc9..1306a75 100644 --- a/keycloak_client.go +++ b/keycloak_client.go @@ -13,40 +13,64 @@ import ( "gopkg.in/h2non/gentleman.v2/plugin" "gopkg.in/h2non/gentleman.v2/plugins/query" "gopkg.in/h2non/gentleman.v2/plugins/timeout" + + jwt "github.com/gbrlsnchs/jwt" ) // Config is the keycloak client http config. type Config struct { - Addr string - Timeout time.Duration + AddrTokenProvider string + AddrAPI string + Timeout time.Duration } // Client is the keycloak client. type Client struct { - url *url.URL - httpClient *gentleman.Client + tokenProviderURL *url.URL + apiURL *url.URL + httpClient *gentleman.Client +} + +// HTTPError is returned when an error occured while contacting the keycloak instance. +type HTTPError struct { + HTTPStatus int + Message string +} + +func (e HTTPError) Error() string { + return fmt.Sprintf("Error %d: %s", e.HTTPStatus, e.Message) } // New returns a keycloak client. func New(config Config) (*Client, error) { - var u *url.URL + var uToken *url.URL + { + var err error + uToken, err = url.Parse(config.AddrTokenProvider) + if err != nil { + return nil, errors.Wrap(err, "could not parse Token Provider URL") + } + } + + var uAPI *url.URL { var err error - u, err = url.Parse(config.Addr) + uAPI, err = url.Parse(config.AddrAPI) if err != nil { - return nil, errors.Wrap(err, "could not parse URL") + return nil, errors.Wrap(err, "could not parse API URL") } } var httpClient = gentleman.New() { - httpClient = httpClient.URL(u.String()) + httpClient = httpClient.URL(uAPI.String()) httpClient = httpClient.Use(timeout.Request(config.Timeout)) } return &Client{ - url: u, - httpClient: httpClient, + tokenProviderURL: uToken, + apiURL: uAPI, + httpClient: httpClient, }, nil } @@ -90,6 +114,9 @@ func (c *Client) GetToken(realm string, username string, password string) (strin } } + fmt.Printf("%s", accessToken.(string)) + fmt.Println() + return accessToken.(string), nil } @@ -98,7 +125,7 @@ func (c *Client) VerifyToken(realmName string, accessToken string) error { var oidcProvider *oidc.Provider { var err error - var issuer = fmt.Sprintf("%s/auth/realms/%s", c.url.String(), realmName) + var issuer = fmt.Sprintf("%s/auth/realms/%s", c.tokenProviderURL.String(), realmName) oidcProvider, err = oidc.NewProvider(context.Background(), issuer) if err != nil { return errors.Wrap(err, "could not create oidc provider") @@ -114,9 +141,14 @@ func (c *Client) VerifyToken(realmName string, accessToken string) error { // get is a HTTP get method. func (c *Client) get(accessToken string, data interface{}, plugins ...plugin.Plugin) error { + var err error var req = c.httpClient.Get() + req = applyPlugins(req, plugins...) + req, err = setAuthorisationAndHostHeaders(req, accessToken) - req = applyPlugins(req, accessToken, plugins...) + if err != nil { + return err + } var resp *gentleman.Response { @@ -128,9 +160,15 @@ func (c *Client) get(accessToken string, data interface{}, plugins ...plugin.Plu switch { case resp.StatusCode == http.StatusUnauthorized: - return fmt.Errorf("unauthorized request: '%v': %v", resp.RawResponse.Status, resp.String()) + return HTTPError{ + HTTPStatus: resp.StatusCode, + Message: string(resp.Bytes()), + } case resp.StatusCode >= 400: - return fmt.Errorf("invalid status code: '%v': %v", resp.RawResponse.Status, resp.String()) + return HTTPError{ + HTTPStatus: resp.StatusCode, + Message: string(resp.Bytes()), + } case resp.StatusCode >= 200: switch resp.Header.Get("Content-Type") { case "application/json": @@ -147,41 +185,62 @@ func (c *Client) get(accessToken string, data interface{}, plugins ...plugin.Plu } } -func (c *Client) post(accessToken string, data interface{}, plugins ...plugin.Plugin) error { +func (c *Client) post(accessToken string, data interface{}, plugins ...plugin.Plugin) (string, error) { + var err error var req = c.httpClient.Post() - req = applyPlugins(req, accessToken, plugins...) + req = applyPlugins(req, plugins...) + req, err = setAuthorisationAndHostHeaders(req, accessToken) + + if err != nil { + return "", err + } + var resp *gentleman.Response { var err error resp, err = req.Do() if err != nil { - return errors.Wrap(err, "could not get response") + return "", errors.Wrap(err, "could not get response") } switch { case resp.StatusCode == http.StatusUnauthorized: - return fmt.Errorf("unauthorized request: '%v': %v", resp.RawResponse.Status, resp.String()) + return "", HTTPError{ + HTTPStatus: resp.StatusCode, + Message: string(resp.Bytes()), + } case resp.StatusCode >= 400: - return fmt.Errorf("invalid status code: '%v': %v", resp.RawResponse.Status, string(resp.Bytes())) + return "", HTTPError{ + HTTPStatus: resp.StatusCode, + Message: string(resp.Bytes()), + } case resp.StatusCode >= 200: + var location = resp.Header.Get("Location") + switch resp.Header.Get("Content-Type") { case "application/json": - return resp.JSON(data) + return location, resp.JSON(data) case "application/octet-stream": data = resp.Bytes() - return nil + return location, nil default: - return nil + return location, nil } default: - return fmt.Errorf("unknown response status code: %v", resp.StatusCode) + return "", fmt.Errorf("unknown response status code: %v", resp.StatusCode) } } } func (c *Client) delete(accessToken string, plugins ...plugin.Plugin) error { + var err error var req = c.httpClient.Delete() - req = applyPlugins(req, accessToken, plugins...) + req = applyPlugins(req, plugins...) + req, err = setAuthorisationAndHostHeaders(req, accessToken) + + if err != nil { + return err + } var resp *gentleman.Response { @@ -193,20 +252,35 @@ func (c *Client) delete(accessToken string, plugins ...plugin.Plugin) error { switch { case resp.StatusCode == http.StatusUnauthorized: - return fmt.Errorf("unauthorized request: '%v': %v", resp.RawResponse.Status, resp.String()) + return HTTPError{ + HTTPStatus: resp.StatusCode, + Message: string(resp.Bytes()), + } case resp.StatusCode >= 400: - return fmt.Errorf("invalid status code: '%v': %v", resp.RawResponse.Status, string(resp.Bytes())) + return HTTPError{ + HTTPStatus: resp.StatusCode, + Message: string(resp.Bytes()), + } case resp.StatusCode >= 200: return nil default: - return fmt.Errorf("unknown response status code: %v", resp.StatusCode) + return HTTPError{ + HTTPStatus: resp.StatusCode, + Message: string(resp.Bytes()), + } } } } func (c *Client) put(accessToken string, plugins ...plugin.Plugin) error { + var err error var req = c.httpClient.Put() - req = applyPlugins(req, accessToken, plugins...) + req = applyPlugins(req, plugins...) + req, err = setAuthorisationAndHostHeaders(req, accessToken) + + if err != nil { + return err + } var resp *gentleman.Response { @@ -218,26 +292,85 @@ func (c *Client) put(accessToken string, plugins ...plugin.Plugin) error { switch { case resp.StatusCode == http.StatusUnauthorized: - return fmt.Errorf("unauthorized request: '%v': %v", resp.RawResponse.Status, resp.String()) + return HTTPError{ + HTTPStatus: resp.StatusCode, + Message: string(resp.Bytes()), + } case resp.StatusCode >= 400: - return fmt.Errorf("invalid status code: '%v': %v", resp.RawResponse.Status, string(resp.Bytes())) + return HTTPError{ + HTTPStatus: resp.StatusCode, + Message: string(resp.Bytes()), + } case resp.StatusCode >= 200: return nil default: - return fmt.Errorf("unknown response status code: %v", resp.StatusCode) + return HTTPError{ + HTTPStatus: resp.StatusCode, + Message: string(resp.Bytes()), + } } } } -// applyPlugins apply all the plugins to the request req. -func applyPlugins(req *gentleman.Request, accessToken string, plugins ...plugin.Plugin) *gentleman.Request { +func setAuthorisationAndHostHeaders(req *gentleman.Request, accessToken string) (*gentleman.Request, error) { + host, err := extractHostFromToken(accessToken) + + if err != nil { + return req, err + } + var r = req.SetHeader("Authorization", fmt.Sprintf("Bearer %s", accessToken)) + r = r.SetHeader("X-Forwarded-Proto", "https") + + r.Context.Request.Host = host + + return r, nil +} + +// applyPlugins apply all the plugins to the request req. +func applyPlugins(req *gentleman.Request, plugins ...plugin.Plugin) *gentleman.Request { + var r = req for _, p := range plugins { r = r.Use(p) } return r } +func extractHostFromToken(token string) (string, error) { + issuer, err := extractIssuerFromToken(token) + + if err != nil { + return "", err + } + + var u *url.URL + { + var err error + u, err = url.Parse(issuer) + if err != nil { + return "", errors.Wrap(err, "could not parse Token issuer URL") + } + } + + return u.Host, nil +} + +func extractIssuerFromToken(token string) (string, error) { + payload, _, err := jwt.Parse(token) + + if err != nil { + return "", errors.Wrap(err, "could not parse Token") + } + + var jot jwt.JWT + + if err = jwt.Unmarshal(payload, &jot); err != nil { + return "", errors.Wrap(err, "could not unmarshall token") + } + + return jot.Issuer, nil +} + // createQueryPlugins create query parameters with the key values paramKV. func createQueryPlugins(paramKV ...string) []plugin.Plugin { var plugins = []plugin.Plugin{} diff --git a/realm.go b/realm.go index 9e61091..b850bbb 100644 --- a/realm.go +++ b/realm.go @@ -13,14 +13,14 @@ const ( // GetRealms get the top level represention of all the realms. Nested information like users are // not included. -func (c *Client) GetRealms(accessToken string) ([]RealmRepresentation, error){ +func (c *Client) GetRealms(accessToken string) ([]RealmRepresentation, error) { var resp = []RealmRepresentation{} var err = c.get(accessToken, &resp, url.Path(realmRootPath)) return resp, err } // CreateRealm creates the realm from its RealmRepresentation. -func (c *Client) CreateRealm(accessToken string, realm RealmRepresentation) error { +func (c *Client) CreateRealm(accessToken string, realm RealmRepresentation) (string, error) { return c.post(accessToken, nil, url.Path(realmRootPath), body.JSON(realm)) } diff --git a/roles.go b/roles.go new file mode 100644 index 0000000..c0fdbc9 --- /dev/null +++ b/roles.go @@ -0,0 +1,38 @@ +package keycloak + +import ( + "gopkg.in/h2non/gentleman.v2/plugins/body" + "gopkg.in/h2non/gentleman.v2/plugins/url" +) + +const ( + rolePath = "/auth/admin/realms/:realm/roles" + roleByIDPath = "/auth/admin/realms/:realm/roles-by-id/:id" + clientRolePath = "/auth/admin/realms/:realm/clients/:id/roles" +) + +// GetClientRoles gets all roles for the realm or client +func (c *Client) GetClientRoles(accessToken string, realmName, idClient string) ([]RoleRepresentation, error) { + var resp = []RoleRepresentation{} + var err = c.get(accessToken, &resp, url.Path(clientRolePath), url.Param("realm", realmName), url.Param("id", idClient)) + return resp, err +} + +// CreateClientRole creates a new role for the realm or client +func (c *Client) CreateClientRole(accessToken string, realmName, clientID string, role RoleRepresentation) (string, error) { + return c.post(accessToken, nil, url.Path(clientRolePath), url.Param("realm", realmName), url.Param("id", clientID), body.JSON(role)) +} + +// GetRoles gets all roles for the realm or client +func (c *Client) GetRoles(accessToken string, realmName string) ([]RoleRepresentation, error) { + var resp = []RoleRepresentation{} + var err = c.get(accessToken, &resp, url.Path(rolePath), url.Param("realm", realmName)) + return resp, err +} + +// GetRole gets a specific role’s representation +func (c *Client) GetRole(accessToken string, realmName string, roleID string) (RoleRepresentation, error) { + var resp = RoleRepresentation{} + var err = c.get(accessToken, &resp, url.Path(roleByIDPath), url.Param("realm", realmName), url.Param("id", roleID)) + return resp, err +} diff --git a/users.go b/users.go index d6dc7ca..b3c6272 100644 --- a/users.go +++ b/users.go @@ -11,6 +11,8 @@ const ( userPath = "/auth/admin/realms/:realm/users" userCountPath = userPath + "/count" userIDPath = userPath + "/:id" + resetPasswordPath = userIDPath + "/reset-password" + sendVerifyEmailPath = userIDPath + "/send-verify-email" ) // GetUsers returns a list of users, filtered according to the query parameters. @@ -29,7 +31,7 @@ func (c *Client) GetUsers(accessToken string, realmName string, paramKV ...strin } // CreateUser creates the user from its UserRepresentation. The username must be unique. -func (c *Client) CreateUser(accessToken string, realmName string, user UserRepresentation) error { +func (c *Client) CreateUser(accessToken string, realmName string, user UserRepresentation) (string, error) { return c.post(accessToken, nil, url.Path(userPath), url.Param("realm", realmName), body.JSON(user)) } @@ -56,3 +58,19 @@ func (c *Client) UpdateUser(accessToken string, realmName, userID string, user U func (c *Client) DeleteUser(accessToken string, realmName, userID string) error { return c.delete(accessToken, url.Path(userIDPath), url.Param("realm", realmName), url.Param("id", userID)) } + +// ResetPassword resets password of the user. +func (c *Client) ResetPassword(accessToken string, realmName, userID string, cred CredentialRepresentation) error { + return c.put(accessToken, url.Path(resetPasswordPath), url.Param("realm", realmName), url.Param("id", userID), body.JSON(cred)) +} + +// SendVerifyEmail sends an email-verification email to the user An email contains a link the user can click to verify their email address. +func (c *Client) SendVerifyEmail(accessToken string, realmName string, userID string, paramKV ...string) error { + if len(paramKV)%2 != 0 { + return fmt.Errorf("the number of key/val parameters should be even") + } + + var plugins = append(createQueryPlugins(paramKV...), url.Path(sendVerifyEmailPath), url.Param("realm", realmName), url.Param("id", userID)) + + return c.put(accessToken, plugins...) +}