diff --git a/config/config.yml_example_homeassistant b/config/config.yml_example_homeassistant index bbe860e2..724810d5 100644 --- a/config/config.yml_example_homeassistant +++ b/config/config.yml_example_homeassistant @@ -35,3 +35,6 @@ oauth: callback_url: https://vouch.yourdomain.com/auth auth_url: https://homeassistant.yourdomain.com:port/auth/authorize token_url: https://homeassistant.yourdomain.com:port/auth/token + user_info_url: ws://homeassistant.yourdomain.com:port/api/websocket + # or if https: + #user_info_url: wss://homeassistant.yourdomain.com:port/api/websocket diff --git a/go.mod b/go.mod index 9b3e5f69..7d04bd0e 100644 --- a/go.mod +++ b/go.mod @@ -30,6 +30,7 @@ require ( github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/gorilla/securecookie v1.1.2 // indirect + github.com/gorilla/websocket v1.5.3 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/influxdata/tdigest v0.0.1 // indirect github.com/josharian/intern v1.0.0 // indirect diff --git a/go.sum b/go.sum index f1f1b26c..b54be907 100644 --- a/go.sum +++ b/go.sum @@ -28,6 +28,8 @@ github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kX github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= github.com/gorilla/sessions v1.2.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTjY= github.com/gorilla/sessions v1.2.2/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/influxdata/tdigest v0.0.1 h1:XpFptwYmnEKUqmkcDjrzffswZ3nvNeevbUSLPP/ZzIY= diff --git a/pkg/providers/homeassistant/homeassistant.go b/pkg/providers/homeassistant/homeassistant.go index c2b7ad0b..ef61de70 100644 --- a/pkg/providers/homeassistant/homeassistant.go +++ b/pkg/providers/homeassistant/homeassistant.go @@ -11,9 +11,12 @@ OR CONDITIONS OF ANY KIND, either express or implied. package homeassistant import ( - "golang.org/x/oauth2" + "encoding/json" "net/http" + "golang.org/x/oauth2" + + "github.com/gorilla/websocket" "github.com/vouch/vouch-proxy/pkg/cfg" "github.com/vouch/vouch-proxy/pkg/providers/common" "github.com/vouch/vouch-proxy/pkg/structs" @@ -30,15 +33,94 @@ func (Provider) Configure() { log = cfg.Logging.Logger } +type AuthMessage struct { + Type string `json:"type"` + Token string `json:"access_token"` +} + +type AuthResponse struct { + Type string `json:"type"` +} + +type RequestMessage struct { + Id int `json:"id"` + Type string `json:"type"` +} + +type ResponseMessage struct { + Id int `json:"id"` + Success bool `json:"success"` + Result structs.HomeAssistantUser `json:"result"` +} + // GetUserInfo provider specific call to get userinfomation -// More info: https://developers.home-assistant.io/docs/en/auth_api.html +// More info: https://github.com/home-assistant/core/blob/5280291f98db41b6edd822a6b2fe6df4dea3df6a/homeassistant/components/auth/__init__.py#L484 +// Websocket API info: https://developers.home-assistant.io/docs/api/websocket func (Provider) GetUserInfo(r *http.Request, user *structs.User, customClaims *structs.CustomClaims, ptokens *structs.PTokens, opts ...oauth2.AuthCodeOption) (rerr error) { _, providerToken, err := common.PrepareTokensAndClient(r, ptokens, false, opts...) if err != nil { return err } ptokens.PAccessToken = providerToken.Extra("access_token").(string) - // Home assistant does not provide an API to query username, so we statically set it to "homeassistant" - user.Username = "homeassistant" + + client, _, err := websocket.DefaultDialer.Dial(cfg.GenOAuth.UserInfoURL, nil) + if err != nil { + log.Errorf("error dialing HA websocket: %v", err) + return err + } + defer client.Close() + + _, _, err = client.ReadMessage() + if err != nil { + log.Errorf("error reading HA init message: %v", err) + return err + } + + authMessage := AuthMessage{ + Type: "auth", + Token: ptokens.PAccessToken, + } + if err := client.WriteJSON(authMessage); err != nil { + return err + } + _, authResponseData, err := client.ReadMessage() + if err != nil { + return err + } + var authResponse AuthResponse + if err := json.Unmarshal(authResponseData, &authResponse); err != nil { + return err + } + if authResponse.Type != "auth_ok" { + log.Errorf("error authenticating with HA: %s", authResponseData) + return err + } + + requestMessage := RequestMessage{ + Id: 10, // Can be any number but must be increased on each request + Type: "auth/current_user", + } + if err := client.WriteJSON(requestMessage); err != nil { + return err + } + _, responseMessage, err := client.ReadMessage() + if err != nil { + return err + } + log.Infof("HA userinfo body: %s", string(responseMessage)) + if err = common.MapClaims(responseMessage, customClaims); err != nil { + log.Error(err) + return err + } + var data ResponseMessage + if err := json.Unmarshal(responseMessage, &data); err != nil { + return err + } + if !data.Success { + log.Errorf("error getting user info from HA: %s", responseMessage) + return err + } + data.Result.PrepareUserData() + user.Username = data.Result.Username return nil } diff --git a/pkg/structs/structs.go b/pkg/structs/structs.go index bccc0180..6eda6b68 100644 --- a/pkg/structs/structs.go +++ b/pkg/structs/structs.go @@ -130,6 +130,17 @@ func (u *GitHubUser) PrepareUserData() { u.Username = u.Login } +// HomeAssistantUser +type HomeAssistantUser struct { + User + IsAdmin bool `json:"is_admin"` + IsOwner bool `json:"is_owner"` +} + +func (u *HomeAssistantUser) PrepareUserData() { + u.Username = u.Name +} + // IndieAuthUser see indieauth.net type IndieAuthUser struct { User @@ -148,7 +159,7 @@ type Contact struct { Verified bool `json:"is_verified"` } -//OpenStaxUser is a retrieved and authenticated user from OpenStax Accounts +// OpenStaxUser is a retrieved and authenticated user from OpenStax Accounts type OpenStaxUser struct { User Contacts []Contact `json:"contact_infos"`