Skip to content

Commit

Permalink
fix: add device flow logic to hydra client
Browse files Browse the repository at this point in the history
  • Loading branch information
nsklikas committed Apr 5, 2024
1 parent 4ca0d13 commit ca4a1f9
Show file tree
Hide file tree
Showing 7 changed files with 276 additions and 12 deletions.
18 changes: 10 additions & 8 deletions internal/hydra/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,36 +3,38 @@ package hydra
import (
"net/http"

client "github.com/ory/hydra-client-go/v2"
hClient "github.com/ory/hydra-client-go/v2"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
)

type Client struct {
c *client.APIClient
c *hClient.APIClient
deviceApi *DeviceApiService
}

func (c *Client) OAuth2Api() client.OAuth2Api {
return c.c.OAuth2Api
func (c *Client) OAuth2Api() OAuth2Api {
return c.deviceApi
}

func (c *Client) MetadataApi() client.MetadataApi {
func (c *Client) MetadataApi() hClient.MetadataApi {
return c.c.MetadataApi
}

func NewClient(url string, debug bool) *Client {
c := new(Client)

configuration := client.NewConfiguration()
configuration := hClient.NewConfiguration()
configuration.Debug = debug
configuration.Servers = []client.ServerConfiguration{
configuration.Servers = []hClient.ServerConfiguration{
{
URL: url,
},
}

configuration.HTTPClient = &http.Client{Transport: otelhttp.NewTransport(http.DefaultTransport)}

c.c = client.NewAPIClient(configuration)
c.c = hClient.NewAPIClient(configuration)
c.deviceApi = newDeviceApiService(c.c)

return c
}
237 changes: 237 additions & 0 deletions internal/hydra/device.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
package hydra

import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"

hClient "github.com/ory/hydra-client-go/v2"
)

// We implement the device API logic, because the upstream sdk does not support it.
// Otherwise we would have to fork the sdk
// TODO(nsklikas): Remove this once upstream hydra supports the device flow

type DeviceApiService struct {
client *hClient.APIClient
hClient.OAuth2Api
}

type APIError struct {
body []byte
error string
}

// Error returns non-empty string if there was an error.
func (e APIError) Error() string {
return e.error
}

// Body returns the raw bytes of the response
func (e APIError) Body() []byte {
return e.body
}

// Prevent trying to import "fmt"
func reportError(format string, a ...interface{}) error {
return fmt.Errorf(format, a...)
}

// AcceptDeviceUserCodeRequest Contains information on an device verification
type AcceptDeviceUserCodeRequest struct {
UserCode *string `json:"user_code,omitempty"`
}

// NewAcceptDeviceUserCodeRequest instantiates a new AcceptDeviceUserCodeRequest object
// This constructor will assign default values to properties that have it defined,
// and makes sure properties required by API are set, but the set of arguments
// will change when the set of required properties is changed
func NewAcceptDeviceUserCodeRequest() *AcceptDeviceUserCodeRequest {
this := AcceptDeviceUserCodeRequest{}
return &this
}

// NewAcceptDeviceUserCodeRequestWithDefaults instantiates a new AcceptDeviceUserCodeRequest object
// This constructor will only assign default values to properties that have it defined,
// but it doesn't guarantee that properties required by API are set
func NewAcceptDeviceUserCodeRequestWithDefaults() *AcceptDeviceUserCodeRequest {
this := AcceptDeviceUserCodeRequest{}
return &this
}

// GetUserCode returns the UserCode field value if set, zero value otherwise.
func (o *AcceptDeviceUserCodeRequest) GetUserCode() string {
if o == nil || o.UserCode == nil {
var ret string
return ret
}
return *o.UserCode
}

// GetUserCodeOk returns a tuple with the UserCode field value if set, nil otherwise
// and a boolean to check if the value has been set.
func (o *AcceptDeviceUserCodeRequest) GetUserCodeOk() (*string, bool) {
if o == nil || o.UserCode == nil {
return nil, false
}
return o.UserCode, true
}

// HasUserCode returns a boolean if a field has been set.
func (o *AcceptDeviceUserCodeRequest) HasUserCode() bool {
if o != nil && o.UserCode != nil {
return true
}

return false
}

// SetUserCode gets a reference to the given string and assigns it to the UserCode field.
func (o *AcceptDeviceUserCodeRequest) SetUserCode(v string) {
o.UserCode = &v
}

func (o AcceptDeviceUserCodeRequest) MarshalJSON() ([]byte, error) {
toSerialize := map[string]interface{}{}
if o.UserCode != nil {
toSerialize["user_code"] = o.UserCode
}
return json.Marshal(toSerialize)
}

type NullableAcceptDeviceUserCodeRequest struct {
value *AcceptDeviceUserCodeRequest
isSet bool
}

func (v NullableAcceptDeviceUserCodeRequest) Get() *AcceptDeviceUserCodeRequest {
return v.value
}

func (v *NullableAcceptDeviceUserCodeRequest) Set(val *AcceptDeviceUserCodeRequest) {
v.value = val
v.isSet = true
}

func (v NullableAcceptDeviceUserCodeRequest) IsSet() bool {
return v.isSet
}

func (v *NullableAcceptDeviceUserCodeRequest) Unset() {
v.value = nil
v.isSet = false
}

func NewNullableAcceptDeviceUserCodeRequest(val *AcceptDeviceUserCodeRequest) *NullableAcceptDeviceUserCodeRequest {
return &NullableAcceptDeviceUserCodeRequest{value: val, isSet: true}
}

func (v NullableAcceptDeviceUserCodeRequest) MarshalJSON() ([]byte, error) {
return json.Marshal(v.value)
}

func (v *NullableAcceptDeviceUserCodeRequest) UnmarshalJSON(src []byte) error {
v.isSet = true
return json.Unmarshal(src, &v.value)
}

type ApiAcceptUserCodeRequestRequest struct {
ctx context.Context
ApiService OAuth2Api
deviceChallenge *string
acceptDeviceUserCodeRequest *AcceptDeviceUserCodeRequest
}

func (r ApiAcceptUserCodeRequestRequest) DeviceChallenge(deviceChallenge string) ApiAcceptUserCodeRequestRequest {
r.deviceChallenge = &deviceChallenge
return r
}

func (r ApiAcceptUserCodeRequestRequest) AcceptDeviceUserCodeRequest(acceptDeviceUserCodeRequest AcceptDeviceUserCodeRequest) ApiAcceptUserCodeRequestRequest {
r.acceptDeviceUserCodeRequest = &acceptDeviceUserCodeRequest
return r
}

func (r ApiAcceptUserCodeRequestRequest) Execute() (*hClient.OAuth2RedirectTo, *http.Response, error) {
return r.ApiService.AcceptUserCodeRequestExecute(r)
}

/*
AcceptUserCodeRequest Accepts a device grant user_code request
Accepts a device grant user_code request
@param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background().
@return ApiAcceptUserCodeRequestRequest
*/
func (a *DeviceApiService) AcceptUserCodeRequest(ctx context.Context) ApiAcceptUserCodeRequestRequest {
return ApiAcceptUserCodeRequestRequest{
ApiService: a,
ctx: ctx,
}
}

// Execute executes the request
//
// @return OAuth2RedirectTo
func (a *DeviceApiService) AcceptUserCodeRequestExecute(r ApiAcceptUserCodeRequestRequest) (*hClient.OAuth2RedirectTo, *http.Response, error) {
body, err := json.Marshal(r.acceptDeviceUserCodeRequest)
if err != nil {
return nil, nil, err
}

localBasePath, err := a.client.GetConfig().ServerURLWithContext(r.ctx, "OAuth2ApiService.AcceptUserCodeRequest")
if err != nil {
return nil, nil, err
}
url := localBasePath + "/admin/oauth2/auth/requests/device/accept"

req, err := http.NewRequest(http.MethodPut, url, bytes.NewBuffer(body))
if err != nil {
return nil, nil, reportError("error when constructing request: %s", err)
}

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

query := req.URL.Query()
query.Add("challenge", *r.deviceChallenge)
req.URL.RawQuery = query.Encode()

client := a.client.GetConfig().HTTPClient
resp, err := client.Do(req)
if err != nil {
return nil, nil, reportError("failed to verify device code: %s", err)
}
defer resp.Body.Close()

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

if resp.StatusCode >= 300 {
newErr := &APIError{
body: respBody,
error: resp.Status,
}
return nil, resp, newErr
}

acceptDeviceResponse := new(hClient.OAuth2RedirectTo)
err = json.Unmarshal(respBody, acceptDeviceResponse)
if err != nil {
return nil, resp, reportError("error when parsing request body: %s", err)
}

return acceptDeviceResponse, resp, nil
}

func newDeviceApiService(api *hClient.APIClient) *DeviceApiService {
a := new(DeviceApiService)
a.client = api
a.OAuth2Api = api.OAuth2Api
return a
}
21 changes: 21 additions & 0 deletions internal/hydra/interfaces.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package hydra

import (
"context"
"net/http"

hClient "github.com/ory/hydra-client-go/v2"
)

// We implement the device API logic, because the upstream sdk does not support it.
// Otherwise we would have to fork the sdk.
// TODO(nsklikas): Remove this once upstream hydra supports the device flow
type DeviceApi interface {
AcceptUserCodeRequest(context.Context) ApiAcceptUserCodeRequestRequest
AcceptUserCodeRequestExecute(ApiAcceptUserCodeRequestRequest) (*hClient.OAuth2RedirectTo, *http.Response, error)
}

type OAuth2Api interface {
hClient.OAuth2Api
DeviceApi
}
4 changes: 3 additions & 1 deletion pkg/extra/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,16 @@ import (

hClient "github.com/ory/hydra-client-go/v2"
kClient "github.com/ory/kratos-client-go"

"github.com/canonical/identity-platform-login-ui/internal/hydra"
)

type KratosClientInterface interface {
FrontendApi() kClient.FrontendApi
}

type HydraClientInterface interface {
OAuth2Api() hClient.OAuth2Api
OAuth2Api() hydra.OAuth2Api
}

type ServiceInterface interface {
Expand Down
2 changes: 1 addition & 1 deletion pkg/extra/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import (
//go:generate mockgen -build_flags=--mod=mod -package extra -destination ./mock_monitor.go -source=../../internal/monitoring/interfaces.go
//go:generate mockgen -build_flags=--mod=mod -package extra -destination ./mock_tracing.go -source=../../internal/tracing/interfaces.go
//go:generate mockgen -build_flags=--mod=mod -package extra -destination ./mock_kratos.go github.com/ory/kratos-client-go FrontendApi
//go:generate mockgen -build_flags=--mod=mod -package extra -destination ./mock_hydra.go github.com/ory/hydra-client-go/v2 OAuth2Api
//go:generate mockgen -build_flags=--mod=mod -package extra -destination ./mock_hydra.go -source=../../internal/hydra/interfaces.go

func TestCheckSessionSuccess(t *testing.T) {
ctrl := gomock.NewController(t)
Expand Down
4 changes: 3 additions & 1 deletion pkg/kratos/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,16 @@ import (

hClient "github.com/ory/hydra-client-go/v2"
kClient "github.com/ory/kratos-client-go"

"github.com/canonical/identity-platform-login-ui/internal/hydra"
)

type KratosClientInterface interface {
FrontendApi() kClient.FrontendApi
}

type HydraClientInterface interface {
OAuth2Api() hClient.OAuth2Api
OAuth2Api() hydra.OAuth2Api
}

type AuthorizerInterface interface {
Expand Down
2 changes: 1 addition & 1 deletion pkg/kratos/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import (
//go:generate mockgen -build_flags=--mod=mod -package kratos -destination ./mock_monitor.go -source=../../internal/monitoring/interfaces.go
//go:generate mockgen -build_flags=--mod=mod -package kratos -destination ./mock_tracing.go -source=../../internal/tracing/interfaces.go
//go:generate mockgen -build_flags=--mod=mod -package kratos -destination ./mock_kratos.go github.com/ory/kratos-client-go FrontendApi
//go:generate mockgen -build_flags=--mod=mod -package kratos -destination ./mock_hydra.go github.com/ory/hydra-client-go/v2 OAuth2Api
//go:generate mockgen -build_flags=--mod=mod -package kratos -destination ./mock_hydra.go -source=../../internal/hydra/interfaces.go

func TestCheckSessionSuccess(t *testing.T) {
ctrl := gomock.NewController(t)
Expand Down

0 comments on commit ca4a1f9

Please sign in to comment.