Skip to content

Commit

Permalink
Add support for Keycloak OIDC (#47)
Browse files Browse the repository at this point in the history
* Add support for KYPO Keycloak OIDC provider

* Add support for refreshing token

* Autodetect Keycloak OIDC instead of provider argument

* Generate docs

---------

Co-authored-by: Zdenek Vydra <[email protected]>
  • Loading branch information
vydrazde and Zdenek Vydra authored Oct 2, 2023
1 parent 90e2ee4 commit 76f0891
Show file tree
Hide file tree
Showing 5 changed files with 95 additions and 19 deletions.
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ provider "kypo" {

### Optional

- `client_id` (String) KYPO local OIDC client ID. Will be ignored when `token` is set. Can be set with `KYPO_CLIENT_ID` environmental variable. See [how to get KYPO client_id](https://github.com/vydrazde/terraform-provider-kypo/wiki/How-to-get-KYPO-CRP-client_id).
- `client_id` (String) KYPO local OIDC client ID. Will be ignored when `token` is set. Defaults to `KYPO-Client`. Can be set with `KYPO_CLIENT_ID` environmental variable. See [how to get KYPO client_id](https://github.com/vydrazde/terraform-provider-kypo/wiki/How-to-get-KYPO-CRP-client_id).
- `endpoint` (String) URI of the homepage of the KYPO instance, like `https://my.kypo.instance.ex`. Can be set with `KYPO_ENDPOINT` environmental variable.
- `password` (String, Sensitive) `password` of the user to login as with `username`. Use either `username` and `password` or just `token`. Can be set with `KYPO_PASSWORD` environmental variable.
- `token` (String, Sensitive) Bearer token to be used. Takes precedence before `username` and `password`. Bearer tokens usually have limited lifespan. Can be set with `KYPO_TOKEN` environmental variable.
Expand Down
74 changes: 74 additions & 0 deletions internal/KYPOClient/auth.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package KYPOClient

import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
Expand All @@ -9,6 +10,7 @@ import (
"net/url"
"regexp"
"strings"
"time"
)

func (c *Client) signIn() (string, error) {
Expand Down Expand Up @@ -166,3 +168,75 @@ func (c *Client) authorizeFirstTime(httpClient http.Client, csrf string) (string
}
return token, err
}

func (c *Client) authenticateKeycloak() error {
query := url.Values{}
query.Add("username", c.Username)
query.Add("password", c.Password)
query.Add("client_id", c.ClientID)
query.Add("grant_type", "password")

req, err := http.NewRequest("POST", fmt.Sprintf("%s/keycloak/realms/KYPO/protocol/openid-connect/token",
c.Endpoint), strings.NewReader(query.Encode()))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
if res.StatusCode == http.StatusNotFound || res.StatusCode == http.StatusMethodNotAllowed {
return &ErrNotFound{ResourceName: "KYPO Keycloak endpoint"}
}
if res.StatusCode != http.StatusOK {
return fmt.Errorf("authenticateKeycloak failed, got HTTP code: %d", res.StatusCode)
}

result := struct {
AccessToken string `json:"access_token"`
ExpiresIn int `json:"expires_in"`
}{}

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

err = json.Unmarshal(body, &result)
if err != nil {
return err
}

c.Token = result.AccessToken
c.TokenExpiryTime = time.Now().Add(time.Duration(result.ExpiresIn) * time.Second)

return nil
}

func (c *Client) authenticate() error {
err := c.authenticateKeycloak()
var errNotFound *ErrNotFound
if errors.As(err, &errNotFound) {
var token string
token, err = c.signIn()
if err != nil {
return err
}
c.Token = token
return nil
}

if err != nil {
return err
}
return nil
}

func (c *Client) refreshToken() error {
if !c.TokenExpiryTime.IsZero() && time.Now().Add(10*time.Second).After(c.TokenExpiryTime) {
return c.authenticateKeycloak()
}
return nil
}
17 changes: 9 additions & 8 deletions internal/KYPOClient/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,17 @@ package KYPOClient

import (
"net/http"
"time"
)

type Client struct {
Endpoint string
ClientID string
HTTPClient *http.Client
Token string
Username string
Password string
Endpoint string
ClientID string
HTTPClient *http.Client
Token string
TokenExpiryTime time.Time
Username string
Password string
}

func NewClientWithToken(endpoint, clientId, token string) (*Client, error) {
Expand All @@ -32,10 +34,9 @@ func NewClient(endpoint, clientId, username, password string) (*Client, error) {
Username: username,
Password: password,
}
token, err := client.signIn()
err := client.authenticate()
if err != nil {
return nil, err
}
client.Token = token
return &client, nil
}
5 changes: 5 additions & 0 deletions internal/KYPOClient/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ type UserModel struct {
}

func (c *Client) doRequest(req *http.Request) ([]byte, int, error) {
err := c.refreshToken()
if err != nil {
return nil, 0, err
}

req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+c.Token)

Expand Down
16 changes: 6 additions & 10 deletions internal/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ func (p *KypoProvider) Schema(_ context.Context, _ provider.SchemaRequest, resp
Sensitive: true,
},
"client_id": schema.StringAttribute{
MarkdownDescription: "KYPO local OIDC client ID. Will be ignored when `token` is set. Can be set with `KYPO_CLIENT_ID` environmental variable. See [how to get KYPO client_id](https://github.com/vydrazde/terraform-provider-kypo/wiki/How-to-get-KYPO-CRP-client_id).",
MarkdownDescription: "KYPO local OIDC client ID. Will be ignored when `token` is set. Defaults to `KYPO-Client`. Can be set with `KYPO_CLIENT_ID` environmental variable. See [how to get KYPO client_id](https://github.com/vydrazde/terraform-provider-kypo/wiki/How-to-get-KYPO-CRP-client_id).",
Optional: true,
},
},
Expand Down Expand Up @@ -140,6 +140,10 @@ func (p *KypoProvider) Configure(ctx context.Context, req provider.ConfigureRequ
clientId = data.ClientID.ValueString()
}

if clientId == "" {
clientId = "KYPO-Client"
}

// If any of the expected configurations are missing, return
// errors with provider-specific guidance.
if endpoint == "" {
Expand All @@ -151,15 +155,6 @@ func (p *KypoProvider) Configure(ctx context.Context, req provider.ConfigureRequ
"If either is already set, ensure the value is not empty.",
)
}
if clientId == "" && token == "" {
resp.Diagnostics.AddAttributeError(
path.Root("client_id"),
"Missing KYPO API Client ID",
"The provider cannot create the KYPO API client as there is a missing or empty value for the KYPO API client ID. "+
"Set the host value in the configuration or use the KYPO_CLIENT_ID environment variable. "+
"If either is already set, ensure the value is not empty.",
)
}
if token == "" && (username == "" || password == "") {
resp.Diagnostics.AddError(
"Missing KYPO API Token or Username and Password",
Expand All @@ -181,6 +176,7 @@ func (p *KypoProvider) Configure(ctx context.Context, req provider.ConfigureRequ

tflog.Debug(ctx, "Creating KYPO client")
var client *KYPOClient.Client

var err error
if token != "" {
client, err = KYPOClient.NewClientWithToken(endpoint, clientId, token)
Expand Down

0 comments on commit 76f0891

Please sign in to comment.