Skip to content

Commit

Permalink
Server side fixes (#12)
Browse files Browse the repository at this point in the history
* fixed determining the root URL
webTicket URL extracted from WWW-AUTHENTICATE header

* set users from the same enterprise as leaders in created meetings

* handle split-domain scenario in the server version of the plugin

* v0.1.2

* split-domain scenario fix

* split-domain scenario fixes

* version 0.1.2-1

* refactoring + tests

* v0.1.2
  • Loading branch information
kosgrz authored Jul 8, 2019
1 parent e42abaf commit fe482a5
Show file tree
Hide file tree
Showing 10 changed files with 507 additions and 103 deletions.
2 changes: 1 addition & 1 deletion plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"id": "skype4business",
"name": "Skype for Business",
"description": "Skype for Business plugin for Mattermost 5.2+.",
"version": "0.1.1",
"version": "0.1.2",
"server": {
"executables": {
"linux-amd64": "server/dist/plugin-linux-amd64",
Expand Down
25 changes: 23 additions & 2 deletions server/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ import (
"bytes"
"crypto/tls"
"encoding/json"
"github.com/pkg/errors"
"io"
"io/ioutil"
"net/http"
"net/url"

"github.com/pkg/errors"
"strings"
)

type Client struct {
Expand Down Expand Up @@ -108,6 +108,27 @@ func (c *Client) newRequest(method, url string, body interface{}, token *string)
return req, nil
}

func (c *Client) performRequestAndGetAuthHeader(url string) (*string, error) {
req, err := c.newRequest("GET", url, nil, nil)
if err != nil {
return nil, err
}

resp, err := c.httpClient.Do(req)
if err != nil {
return nil, err
}

for k, v := range resp.Header {
if strings.ToUpper(k) == "WWW-AUTHENTICATE" {
authHeader := strings.Join(v, ",")
return &authHeader, nil
}
}

return nil, errors.New("Response doesn't have WWW-AUTHENTICATE header!")
}

func (c *Client) do(req *http.Request, v interface{}) (*http.Response, error) {
resp, err := c.httpClient.Do(req)
if err != nil {
Expand Down
63 changes: 49 additions & 14 deletions server/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,23 @@ import (
)

const (
URL_AUTHENTICATE = "/auth"
URL_AUTHENTICATE_FAILING = "/auth_fail"
URL_CREATE_NEW_APP = "/new_app"
URL_CREATE_NEW_APP_FAILING = "/new_app_fail"
URL_CREATE_NEW_MEETING = "/new_meeting"
URL_PERFORM_DISCOVERY = "/discovery"
URL_READ_USER_RESOURCE = "/user"
URL_INVALID = "invalid://u r l"
TEST_TOKEN = "testtoken"
TEST_MY_ONLINE_MEETINGS_URL = "/ucwa/oauth/v1/applications/123/onlineMeetings/myOnlineMeetings"
TEST_ONLINE_MEETING_ID = "FRA03I2T"
TEST_JOIN_URL = "https://test.com/testcompany/testuser/FRA03I2T"
TEST_USER_URL = "https://dc2.testcompany.com/Autodiscover/AutodiscoverService.svc/root/oauth/user"
TEST_APPLICATIONS_URL = "https://dc2.testcompany.com/ucwa/oauth/v1/applications"
URL_AUTHENTICATE = "/auth"
URL_AUTHENTICATE_FAILING = "/auth_fail"
URL_CREATE_NEW_APP = "/new_app"
URL_CREATE_NEW_APP_FAILING = "/new_app_fail"
URL_CREATE_NEW_MEETING = "/new_meeting"
URL_PERFORM_DISCOVERY = "/discovery"
URL_RESPONSE_WITH_AUTH_HEADER = "/response_with_auth_header"
URL_RESPONSE_WITHOUT_AUTH_HEADER = "/response_without_auth_header"
URL_READ_USER_RESOURCE = "/user"
URL_INVALID = "invalid://u r l"
TEST_TOKEN = "testtoken"
TEST_MY_ONLINE_MEETINGS_URL = "/ucwa/oauth/v1/applications/123/onlineMeetings/myOnlineMeetings"
TEST_ONLINE_MEETING_ID = "FRA03I2T"
TEST_JOIN_URL = "https://test.com/testcompany/testuser/FRA03I2T"
TEST_USER_URL = "https://dc2.testcompany.com/Autodiscover/AutodiscoverService.svc/root/oauth/user"
TEST_APPLICATIONS_URL = "https://dc2.testcompany.com/ucwa/oauth/v1/applications"
TEST_AUTH_HEADER = "test_auth_header"
)

var (
Expand Down Expand Up @@ -111,6 +114,29 @@ func TestClient(t *testing.T) {
assert.Nil(t, r)
})

t.Run("test performRequestAndGetAuthHeader", func(t *testing.T) {

r, err := client.performRequestAndGetAuthHeader(server.URL + URL_RESPONSE_WITH_AUTH_HEADER)

assert.Nil(t, err)
assert.NotNil(t, r)
assert.Equal(t, TEST_AUTH_HEADER, *r)

r, err = client.performRequestAndGetAuthHeader(URL_INVALID)
assert.NotNil(t, err)
assert.Nil(t, r)

r, err = client.performRequestAndGetAuthHeader("")
assert.NotNil(t, err)
assert.Nil(t, r)

r, err = client.performRequestAndGetAuthHeader(server.URL + URL_RESPONSE_WITHOUT_AUTH_HEADER)

assert.NotNil(t, err)
assert.Equal(t, "Response doesn't have WWW-AUTHENTICATE header!", err.Error())
assert.Nil(t, r)
})

t.Run("test readUserResource", func(t *testing.T) {

r, err := client.readUserResource(server.URL+URL_READ_USER_RESOURCE, TEST_TOKEN)
Expand Down Expand Up @@ -227,6 +253,15 @@ func setupTestServer(t *testing.T) {
`)
})

mux.HandleFunc(URL_RESPONSE_WITH_AUTH_HEADER, func(writer http.ResponseWriter, request *http.Request) {
writer.Header().Set("WWW-AUTHENTICATE", TEST_AUTH_HEADER)
writer.WriteHeader(http.StatusOK)
})

mux.HandleFunc(URL_RESPONSE_WITHOUT_AUTH_HEADER, func(writer http.ResponseWriter, request *http.Request) {
writer.WriteHeader(http.StatusOK)
})

mux.HandleFunc(URL_READ_USER_RESOURCE, func(writer http.ResponseWriter, request *http.Request) {
writeResponse(t, writer, `
{
Expand Down
2 changes: 1 addition & 1 deletion server/manifest.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ var manifest = struct {
Version string
}{
Id: "skype4business",
Version: "0.1.1",
Version: "0.1.2",
}
3 changes: 2 additions & 1 deletion server/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ type State struct {
}

type NewMeetingRequest struct {
Subject string `json:"subject"`
Subject string `json:"subject"`
AutomaticLeaderAssignment string `json:"automaticLeaderAssignment"`
}

type NewMeetingResponse struct {
Expand Down
169 changes: 151 additions & 18 deletions server/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,15 @@ const (
NEW_APPLICATION_USER_AGENT = "mm_skype4b_plugin"
NEW_APPLICATION_CULTURE = "en-US"
WS_EVENT_AUTHENTICATED = "authenticated"
ROOT_URL_KEY = "root_url"
)

type IClient interface {
authenticate(url string, body url.Values) (*AuthResponse, error)
createNewApplication(url string, body interface{}, token string) (*NewApplicationResponse, error)
createNewMeeting(url string, body interface{}, token string) (*NewMeetingResponse, error)
performDiscovery(url string) (*DiscoveryResponse, error)
performRequestAndGetAuthHeader(url string) (*string, error)
readUserResource(url string, token string) (*UserResourceResponse, error)
}

Expand Down Expand Up @@ -368,7 +370,8 @@ func (p *Plugin) handleCreateMeetingInServerVersion(w http.ResponseWriter, r *ht
newMeetingResponse, err := p.client.createNewMeeting(
applicationState.OnlineMeetingsUrl,
NewMeetingRequest{
Subject: "Meeting created by " + user.Username,
Subject: "Meeting created by " + user.Username,
AutomaticLeaderAssignment: "SameEnterprise",
},
applicationState.Token,
)
Expand Down Expand Up @@ -437,10 +440,12 @@ func (p *Plugin) handleProfileImage(w http.ResponseWriter, r *http.Request) {
}

func (p *Plugin) fetchOnlineMeetingsUrl() (*ApplicationState, *APIError) {
config := p.getConfiguration()
discoveryUrl := "https://lyncdiscover." + config.Domain
rootUrl, apiErr := p.getRootUrl()
if apiErr != nil {
return nil, apiErr
}

applicationState, apiError := p.getApplicationState(discoveryUrl)
applicationState, apiError := p.getApplicationState(*rootUrl)
if apiError != nil {
return nil, apiError
}
Expand Down Expand Up @@ -471,18 +476,19 @@ func (p *Plugin) getApplicationState(discoveryUrl string) (*ApplicationState, *A
return nil, &APIError{Message: "Error performing autodiscovery: " + err.Error()}
}

userResourceUrl := DiscoveryResponse.Links.User.Href
resourceRegex := regexp.MustCompile(`https:\/\/(.*)\/Autodiscover\/`)
resourceRegexMatch := resourceRegex.FindStringSubmatch(userResourceUrl)
resourceName := resourceRegexMatch[1]
tokenUrl := "https://lyncweb." + config.Domain + "/webticket/oauthtoken"
authHeader, err := p.client.performRequestAndGetAuthHeader(DiscoveryResponse.Links.User.Href)
if err != nil {
return nil, &APIError{Message: "Error performing request to get authentication header: " + err.Error()}
}

authResponse, err := p.client.authenticate(tokenUrl, url.Values{
"grant_type": {"password"},
"username": {config.Username},
"password": {config.Password},
"resource": {resourceName},
})
tokenUrl, apiErr := p.extractTokenUrl(*authHeader)
if apiErr != nil {
return nil, apiErr
}

userResourceUrl := DiscoveryResponse.Links.User.Href
resourceName := p.extractResourceNameFromUserUrl(userResourceUrl)
authResponse, err := p.authenticate(*tokenUrl, resourceName, *config)
if err != nil {
return nil, &APIError{Message: "Error during authentication: " + err.Error()}
}
Expand All @@ -492,10 +498,34 @@ func (p *Plugin) getApplicationState(discoveryUrl string) (*ApplicationState, *A
return nil, &APIError{Message: "Error reading user resource: " + err.Error()}
}

if userResourceResponse.Links.Applications.Href != "" {
applicationsUrl := userResourceResponse.Links.Applications.Href
if applicationsUrl != "" {

applicationsResourceName := p.extractResourceNameFromApplicationsUrl(applicationsUrl)
if applicationsResourceName != resourceName {
mlog.Warn("Resource from applications url is not the same as resource name from user url")

authHeader, err := p.client.performRequestAndGetAuthHeader(applicationsUrl)
if err != nil {
return nil, &APIError{
Message: "Error performing request to get authentication header from new resource: " + err.Error(),
}
}

tokenUrl, apiErr := p.extractTokenUrl(*authHeader)
if apiErr != nil {
return nil, apiErr
}

authResponse, err = p.authenticate(*tokenUrl, applicationsResourceName, *config)
if err != nil {
return nil, &APIError{Message: "Error during authentication in new resource: " + err.Error()}
}
}

return &ApplicationState{
ApplicationsUrl: userResourceResponse.Links.Applications.Href,
Resource: resourceName,
ApplicationsUrl: applicationsUrl,
Resource: applicationsResourceName,
Token: authResponse.Access_token,
}, nil
} else if userResourceResponse.Links.Redirect.Href != "" {
Expand All @@ -506,3 +536,106 @@ func (p *Plugin) getApplicationState(discoveryUrl string) (*ApplicationState, *A
}
}
}

func (p *Plugin) getRootUrl() (*string, *APIError) {
rootUrlBytes, appErr := p.API.KVGet(ROOT_URL_KEY)
if appErr != nil {
return nil, &APIError{Message: "Cannot fetch the root url from the database: " + appErr.Error()}
}

if rootUrlBytes != nil {
rootUrl := string(rootUrlBytes)
return &rootUrl, nil
}

rootUrl, err := p.determineRootUrl(p.getConfiguration().Domain)
if err != nil {
return nil, err
}

_ = p.API.KVSet(ROOT_URL_KEY, []byte(*rootUrl))

return rootUrl, nil
}

func (p *Plugin) determineRootUrl(domain string) (*string, *APIError) {
for _, o := range []struct {
url string
name string
}{
{
url: "https://lyncdiscoverinternal." + domain,
name: "internal https",
},
{
url: "https://lyncdiscover." + domain,
name: "external https",
},
{
url: "http://lyncdiscoverinternal." + domain,
name: "internal http",
},
{
url: "http://lyncdiscover." + domain,
name: "external http",
},
} {
_, err := p.client.performDiscovery(o.url)
if err == nil {
return &o.url, nil
} else {
mlog.Warn("Error performing autodiscovery with " + o.name + " root URL: " + err.Error())
}
}

return nil, &APIError{
Message: "Cannot determine root URL. Check if your DNS server has a lyncdiscover or lyncdiscoverinternal record.",
}
}

func (p *Plugin) extractTokenUrl(authHeader string) (*string, *APIError) {
webTicketUrlRegexMatch := regexp.MustCompile(`href=(.*?),`).FindStringSubmatch(authHeader)
if len(webTicketUrlRegexMatch) < 1 {
return nil, &APIError{
Message: "Cannot extract webTicket URL from WWW-AUTHENTICATE header! Full header value: " + authHeader,
}
}
webTicketUrl := strings.ReplaceAll(webTicketUrlRegexMatch[1], "\"", "")

grantTypeRegexMatch := regexp.MustCompile(`grant_type="(.*?)"`).FindStringSubmatch(authHeader)
if len(grantTypeRegexMatch) < 1 {
return nil, &APIError{
Message: "Cannot extract grant types from WWW-AUTHENTICATE header! Full header value: " + authHeader,
}
}
grantTypes := grantTypeRegexMatch[1]

if !regexp.MustCompile("password").MatchString(grantTypes) {
return nil, &APIError{
Message: "WWW-AUTHENTICATE header doesn't have the password grant type! Full header value: " + authHeader,
}
}

return &webTicketUrl, nil
}

func (p *Plugin) authenticate(tokenUrl string, resourceName string, config configuration) (*AuthResponse, error) {
return p.client.authenticate(tokenUrl, url.Values{
"grant_type": {"password"},
"username": {config.Username},
"password": {config.Password},
"resource": {resourceName},
})
}

func (p *Plugin) extractResourceNameFromUserUrl(userUrl string) string {
resourceRegex := regexp.MustCompile(`https:\/\/(.*)\/Autodiscover\/`)
resourceRegexMatch := resourceRegex.FindStringSubmatch(userUrl)
return resourceRegexMatch[1]
}

func (p *Plugin) extractResourceNameFromApplicationsUrl(applicationsUrl string) string {
resourceRegex := regexp.MustCompile(`https:\/\/(.*)\/ucwa\/`)
resourceRegexMatch := resourceRegex.FindStringSubmatch(applicationsUrl)
return resourceRegexMatch[1]
}
Loading

0 comments on commit fe482a5

Please sign in to comment.