Skip to content

Commit

Permalink
Bridge Authentication (#6)
Browse files Browse the repository at this point in the history
  • Loading branch information
thibauult authored Jul 9, 2024
1 parent c53e360 commit d44637d
Show file tree
Hide file tree
Showing 14 changed files with 251 additions and 40 deletions.
54 changes: 51 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,7 @@ import (
func main() {

bridge, err := openhue.NewBridgeDiscovery(openhue.WithTimeout(1 * time.Second)).Discover()
if err != nil {
log.Fatal("Bridge Discovery Error: ", err)
}
openhue.CheckErr(err)

fmt.Println(bridge) // Output: Bridge{instance: "Hue Bridge - 1A3E4F", host: "ecb5fa1a3e4f.local.", ip: "192.168.1.xx"}
}
Expand All @@ -80,6 +78,56 @@ Options:
- `openhue.WithTimeout` allows setting the mDNS discovery timeout. Default value is `5` seconds.
- `openhue.WithDisabledUrlDiscovery` allows disabling the URL discovery.

### Authentication
Bridge authentication has been make simple via the `Authenticator` interface:
```go
package main

import (
"fmt"
"github.com/openhue/openhue-go"
"time"
)

func main() {

bridge, err := openhue.NewBridgeDiscovery(openhue.WithTimeout(1 * time.Second)).Discover()
openhue.CheckErr(err)

authenticator, err := openhue.NewAuthenticator(bridge.IpAddress)
openhue.CheckErr(err)

fmt.Println("Press the link button")

var key string
for len(key) == 0 {

// try to authenticate
apiKey, retry, err := authenticator.Authenticate()

if err != nil && retry {
// link button not pressed
fmt.Printf(".")
time.Sleep(500 * time.Millisecond)
} else if err != nil && !retry {
// there is a real error
openhue.CheckErr(err)
} else {
key = apiKey
}
}

fmt.Println("\n", key)
}
```
In this example, we wait until the link button is pressed on the bridge.
The `Authenticator.Authenticate()` function returns three values:
- `apiKey string` that is not empty when `retry = false` and `err == nil`
- `retry bool` which indicates that the link button has not been pressed
- `err error` which contains the error details

**You can consider the authentication has failed whenever the `retry` value is `false` and the `err` is not `nil`.**

## License
[![GitHub License](https://img.shields.io/github/license/openhue/openhue-cli)](https://github.com/openhue/openhue-cli/blob/main/LICENSE)

Expand Down
82 changes: 82 additions & 0 deletions auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package openhue

import (
"context"
"errors"
"fmt"
"os"
)

// Authenticator defines a service that allows retrieving the Hue API Key
type Authenticator interface {
// Authenticate performs a single authentication request to retrieve an API key.
// It will return ("", true, err != nil) if the link button has not been pressed.
Authenticate() (key string, press bool, err error)
}

type authenticatorImpl struct {
client *ClientWithResponses
deviceType string
generateClientKey bool
}

type authOpt func(b *authenticatorImpl)

func NewAuthenticator(bridgeIP string, opts ...authOpt) (Authenticator, error) {
client, err := newClient(bridgeIP, "")
if err != nil {
return nil, err
}

authenticator := &authenticatorImpl{client: client, generateClientKey: true}

for _, o := range opts {
o(authenticator)
}

if len(authenticator.deviceType) == 0 {
hostName, err := os.Hostname()
if err != nil {
return nil, err
}
authenticator.deviceType = hostName
}

return authenticator, nil
}

func (a *authenticatorImpl) Authenticate() (string, bool, error) {

body := AuthenticateJSONRequestBody{
Devicetype: &a.deviceType,
Generateclientkey: &a.generateClientKey,
}

resp, err := a.client.AuthenticateWithResponse(context.Background(), body)
if err != nil {
return "", false, err
}

if resp.JSON200 == nil {
return "", false, fmt.Errorf("unable to reach the Bridge, verify that the IP is correct")
}

auth := (*resp.JSON200)[0]
if auth.Error != nil {
return "", true, errors.New(*auth.Error.Description)
}

return *auth.Success.Username, false, nil
}

func WithDeviceType(deviceType string) authOpt {
return func(b *authenticatorImpl) {
b.deviceType = deviceType
}
}

func WithGenerateClientKey(generateClientKey bool) authOpt {
return func(b *authenticatorImpl) {
b.generateClientKey = generateClientKey
}
}
8 changes: 4 additions & 4 deletions discovery.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const (
discoveryUrl = "https://discovery.meethue.com"
)

type opt func(b *BridgeDiscovery)
type discOpt func(b *BridgeDiscovery)

type BridgeDiscovery struct {
timeout time.Duration
Expand Down Expand Up @@ -46,7 +46,7 @@ const (
TooManyAttempts BridgeDiscoveryError = "too many attempts to discover the bridge via URL"
)

func NewBridgeDiscovery(opts ...opt) *BridgeDiscovery {
func NewBridgeDiscovery(opts ...discOpt) *BridgeDiscovery {
bd := &BridgeDiscovery{
timeout: defaultTimeout,
allowUrlDiscoveryFallback: true,
Expand Down Expand Up @@ -152,14 +152,14 @@ func urlDiscovery() (*BridgeInfo, error) {
}

// WithTimeout specifies that timeout value for the Bridge Discovery. Default is 5 seconds.
func WithTimeout(timeout time.Duration) opt {
func WithTimeout(timeout time.Duration) discOpt {
return func(b *BridgeDiscovery) {
b.timeout = timeout
}
}

// WithDisabledUrlDiscovery allows disabling the URL discovery process in case the mDNS one failed.
func WithDisabledUrlDiscovery() opt {
func WithDisabledUrlDiscovery() discOpt {
return func(b *BridgeDiscovery) {
b.allowUrlDiscoveryFallback = false
}
Expand Down
7 changes: 3 additions & 4 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,26 +1,25 @@
module github.com/openhue/openhue-go

go 1.22
go 1.21

require (
github.com/grandcat/zeroconf v1.0.0
github.com/oapi-codegen/runtime v1.1.1
github.com/stretchr/testify v1.9.0
gopkg.in/yaml.v3 v3.0.1
)

require (
github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
github.com/cenkalti/backoff v2.2.1+incompatible // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/grandcat/zeroconf v1.0.0 // indirect
github.com/miekg/dns v1.1.61 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect
golang.org/x/crypto v0.25.0 // indirect
golang.org/x/mod v0.19.0 // indirect
golang.org/x/net v0.27.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/sys v0.22.0 // indirect
golang.org/x/tools v0.23.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
3 changes: 1 addition & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ github.com/miekg/dns v1.1.61 h1:nLxbwF3XxhwVSm8g9Dghm9MHPaUZuqhPiGL+675ZmEs=
github.com/miekg/dns v1.1.61/go.mod h1:mnAarhS3nWaW+NVP2wTkYVIZyHNJ098SJZUki3eykwQ=
github.com/oapi-codegen/runtime v1.1.1 h1:EXLHh0DXIJnWhdRPN2w4MXAzFyE4CskzhNLUmtpMYro=
github.com/oapi-codegen/runtime v1.1.1/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
Expand All @@ -29,8 +30,6 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8=
golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
Expand Down
3 changes: 2 additions & 1 deletion go.work.sum
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,6 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pelletier/go-toml/v2 v2.0.9 h1:uH2qQXheeefCCkuBBSLi7jCiSmj3VRh2+Goq2N7Xxu0=
github.com/pelletier/go-toml/v2 v2.0.9/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/schollz/closestmatch v2.1.0+incompatible h1:Uel2GXEpJqOWBrlyI+oY9LTiyyjYS17cCYRqP13/SHk=
Expand Down Expand Up @@ -123,6 +122,8 @@ golang.org/x/arch v0.4.0 h1:A8WCeEWhLwPBKNbFi5Wv5UTCBx5zzubnXDlMOFAzFMc=
golang.org/x/arch v0.4.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
Expand Down
9 changes: 9 additions & 0 deletions helpers.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package openhue

import (
"fmt"
"gopkg.in/yaml.v3"
"log"
"os"
Expand Down Expand Up @@ -34,3 +35,11 @@ func LoadConf() (string, string) {

return c["bridge"].(string), c["key"].(string)
}

// CheckErr prints the msg with the prefix 'Error:' and exits with error code 1. If the msg is a nil, it does nothing.
func CheckErr(msg error) {
if msg != nil {
fmt.Fprintln(os.Stderr, "Error:", msg)
os.Exit(1)
}
}
16 changes: 12 additions & 4 deletions openhue.go
Original file line number Diff line number Diff line change
Expand Up @@ -289,13 +289,21 @@ func (h *Home) UpdateScene(sceneId string, body ScenePut) error {
// This function will also skip SSL verification, as the Philips HUE Bridge exposes a self-signed certificate.
func newClient(bridgeIP, apiKey string) (*ClientWithResponses, error) {

apiKeyAuth := func(ctx context.Context, req *http.Request) error {
req.Header.Set("hue-application-key", apiKey)
return nil
var authFn RequestEditorFn

if len(apiKey) > 0 {
authFn = func(ctx context.Context, req *http.Request) error {
req.Header.Set("hue-application-key", apiKey)
return nil
}
} else {
authFn = func(ctx context.Context, req *http.Request) error {
return nil
}
}

// skip SSL Verification
http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true}

return NewClientWithResponses("https://"+bridgeIP, WithRequestEditorFn(apiKeyAuth))
return NewClientWithResponses("https://"+bridgeIP, WithRequestEditorFn(authFn))
}
38 changes: 38 additions & 0 deletions playground/authenticate/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package main

import (
"fmt"
"github.com/openhue/openhue-go"
"time"
)

func main() {

bridge, err := openhue.NewBridgeDiscovery(openhue.WithTimeout(1 * time.Second)).Discover()
openhue.CheckErr(err)

authenticator, err := openhue.NewAuthenticator(bridge.IpAddress)
openhue.CheckErr(err)

fmt.Println("Press the link button")

var key string
for len(key) == 0 {

// try to authenticate
apiKey, retry, err := authenticator.Authenticate()

if err != nil && retry {
// link button not pressed
fmt.Printf(".")
time.Sleep(500 * time.Millisecond)
} else if err != nil && !retry {
// there is a real error
openhue.CheckErr(err)
} else {
key = apiKey
}
}

fmt.Println("\n", key)
}
5 changes: 1 addition & 4 deletions playground/discovery/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,13 @@ package main
import (
"fmt"
"github.com/openhue/openhue-go"
"log"
"time"
)

func main() {

bridge, err := openhue.NewBridgeDiscovery(openhue.WithTimeout(1 * time.Second)).Discover()
if err != nil {
log.Fatal("Bridge Discovery Error: ", err)
}
openhue.CheckErr(err)

fmt.Println(bridge)
}
15 changes: 11 additions & 4 deletions playground/go.mod
Original file line number Diff line number Diff line change
@@ -1,19 +1,26 @@
module github.com/openhue/openhue-go/playground

go 1.22
go 1.21

replace github.com/openhue/openhue-go => ..

require (
github.com/openhue/openhue-go v0.1.0
)
require github.com/openhue/openhue-go v0.1.0

require (
github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
github.com/cenkalti/backoff v2.2.1+incompatible // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/grandcat/zeroconf v1.0.0 // indirect
github.com/miekg/dns v1.1.61 // indirect
github.com/oapi-codegen/runtime v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/stretchr/testify v1.9.0 // indirect
golang.org/x/mod v0.19.0 // indirect
golang.org/x/net v0.27.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/sys v0.22.0 // indirect
golang.org/x/tools v0.23.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
Loading

0 comments on commit d44637d

Please sign in to comment.