Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add openid oauth provider #3

Merged
merged 4 commits into from
Aug 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ on:
branches:
- main
pull_request:
branches:
- main
workflow_dispatch:

permissions: read-all
permissions: {}

jobs:
package:
Expand Down Expand Up @@ -41,6 +43,9 @@ jobs:
- name: Apply patches
run: for i in ./patch/*.patch; do patch -p1 < "$i"; done

- name: Add overlay
run: cp -R ./patch/overlay/* ./

- name: Build server
uses: ./patch/build-server

Expand Down
27 changes: 27 additions & 0 deletions 0002-enable-openid-provider.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
Import openid oauthprovider and enable in non-enterprise version.

--- a/server/cmd/mattermost/main.go
+++ b/server/cmd/mattermost/main.go
@@ -11,6 +11,7 @@ import (
_ "github.com/mattermost/mattermost/server/v8/channels/app/slashcommands"
// Plugins
_ "github.com/mattermost/mattermost/server/v8/channels/app/oauthproviders/gitlab"
+ _ "github.com/mattermost/mattermost/server/v8/channels/app/oauthproviders/openid"

// Enterprise Imports
_ "github.com/mattermost/mattermost/server/v8/enterprise"
--- a/server/config/client.go
+++ b/server/config/client.go
@@ -326,9 +326,9 @@ func GenerateLimitedClientConfig(c *model.Config, telemetryID string, license *m
props["SamlLoginButtonTextColor"] = ""
props["EnableSignUpWithGoogle"] = "false"
props["EnableSignUpWithOffice365"] = "false"
- props["EnableSignUpWithOpenId"] = "false"
- props["OpenIdButtonText"] = ""
- props["OpenIdButtonColor"] = ""
+ props["EnableSignUpWithOpenId"] = strconv.FormatBool(*c.OpenIdSettings.Enable)
+ props["OpenIdButtonColor"] = *c.OpenIdSettings.ButtonColor
+ props["OpenIdButtonText"] = *c.OpenIdSettings.ButtonText
props["CWSURL"] = ""
props["EnableCustomBrand"] = strconv.FormatBool(*c.TeamSettings.EnableCustomBrand)
props["CustomBrandText"] = *c.TeamSettings.CustomBrandText
13 changes: 0 additions & 13 deletions 0002-replace-gitlab-icon-with-opencampus-icon.patch

This file was deleted.

50 changes: 50 additions & 0 deletions 0003-replace-openid-icon-with-opencampus-icon.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
Replace openid logo with the opencampus icon.

--- a/webapp/channels/src/components/widgets/icons/login_openid_icon.tsx
+++ b/webapp/channels/src/components/widgets/icons/login_openid_icon.tsx
@@ -12,23 +12,31 @@ export default function LoginOpenIdIcon(props: React.HTMLAttributes<HTMLSpanElem
<svg
width='16'
height='16'
- viewBox='0 0 16 16'
+ viewBox='0 0 256 256'
fill='none'
xmlns='http://www.w3.org/2000/svg'
- aria-label={formatMessage({id: 'generic_icons.login.openid', defaultMessage: 'OpenID Icon'})}
+ aria-label={formatMessage({id: 'generic_icons.login.eduhub', defaultMessage: 'Edu Hub Icon'})}
>
- <path
- d='M7.19995 1.2V15.2L9.59995 14V0L7.19995 1.2Z'
- fill='#F48018'
- />
- <path
- d='M15.6652 5.3302L16.0001 8.80002L11.313 7.85391'
- fill='#AEB0B3'
- />
- <path
- d='M10 4.61206V6.19484C11.015 6.37555 12.0439 6.71905 12.768 7.17939L14.4821 6.09046C13.3141 5.34815 11.686 4.82159 10 4.61206ZM2.42378 9.90251C2.42378 8.13669 4.2954 6.64895 6.84567 6.19484V4.61206C2.94414 5.09733 0 7.28065 0 9.90251C0 12.5244 3.08942 14.8262 7.2 15.2V13.6001C4.43386 13.2433 2.42378 11.7657 2.42378 9.90251Z'
- fill='#AEB0B3'
- />
+ <path fill='#000000' d='M88.05,128.68c0-17.16,0-34.32,0-51.48c0-0.73,0-1.46,0.02-2.2c0.04-1.19,0.54-1.66,1.71-1.09
+ c0.51,0.25,1.02,0.5,1.53,0.76c35.13,17.55,70.26,35.1,105.39,52.65c0.51,0.25,1.03,0.49,1.52,0.78c0.86,0.5,1.04,1.03,0,1.55
+ c-0.36,0.18-0.73,0.36-1.09,0.54c-35.2,17.58-70.41,35.17-105.61,52.75c-0.51,0.25-1.02,0.5-1.53,0.76
+ c-1.21,0.62-1.86,0.25-1.91-1.11c-0.03-0.81-0.03-1.63-0.03-2.44C88.05,162.99,88.05,145.84,88.05,128.68z M166.42,128.91
+ c-0.83-0.48-1.16-0.7-1.52-0.88c-19.78-9.86-39.56-19.72-59.35-29.56c-2.2-1.09-2.37-0.98-2.37,1.6c-0.01,19.02-0.01,38.04,0,57.06
+ c0,3.15,0.14,3.24,2.9,1.86c14.62-7.28,29.24-14.57,43.85-21.85C155.3,134.46,160.66,131.79,166.42,128.91z'/>
+ <path fill='#000000' d='M126.94,243c-24.12-0.54-46.47-6.62-66.15-21.16c-2.23-1.65-2.24-1.7,0.14-2.95c3.53-1.85,7.08-3.65,10.57-5.56
+ c1.35-0.74,2.45-0.77,3.82,0.07c17.8,10.9,37.2,15.69,57.97,14.43c21.5-1.3,40.7-8.84,57.26-22.67
+ c18.48-15.43,29.89-35.08,34.23-58.79c0.53-2.87,0.88-5.78,1.2-8.69c0.15-1.33,0.79-1.87,2.04-1.87c3.74,0,7.48,0.02,11.22-0.02
+ c1.75-0.02,2.29,0.85,2.07,2.47c-2.4,17.53-6.71,34.41-16.13,49.69c-11.05,17.92-25.81,31.87-44.25,41.94
+ c-12.36,6.76-25.81,10.1-39.58,12.4C136.57,243.11,131.77,242.73,126.94,243z'/>
+ <path fill='#000000' d='M127.5,15.06c33.65,0.79,62.31,12.94,85.03,38.09c14.88,16.48,23.81,35.91,27.4,57.8c0.37,2.24,0.63,4.5,1.02,6.74
+ c0.31,1.79-0.46,2.28-2.13,2.24c-3.49-0.08-6.99-0.1-10.49,0.01c-1.83,0.05-2.58-0.6-2.78-2.46c-1.79-16.24-7.29-31.15-16.7-44.49
+ c-16.93-23.98-39.97-38.19-69.12-42.01c-23.33-3.06-45.08,1.82-65.1,14.19c-1.35,0.83-2.43,0.9-3.81,0.15
+ c-3.56-1.96-7.16-3.84-10.8-5.65c-2-0.99-1.58-1.68-0.07-2.81c13.03-9.81,27.66-15.91,43.57-19.25
+ C111.43,15.95,119.34,14.72,127.5,15.06z'/>
+ <path fill='#000000' d='M12.66,128.88c1.09-28.72,10.11-54.38,29.38-76.12c1.3-1.47,2.34-1.81,4.12-0.79c3.11,1.76,6.34,3.31,9.58,4.83
+ c1.78,0.83,2.12,1.45,0.56,3.05C45.54,70.91,37.35,83.67,32.93,98.5c-10.5,35.28-4.1,67.27,19.56,95.63
+ c1.25,1.5,2.58,2.92,3.95,4.31c1.01,1.03,0.99,1.75-0.35,2.4c-3.73,1.81-7.46,3.6-11.17,5.46c-1.69,0.85-2.41-0.46-3.2-1.39
+ c-5.65-6.63-10.74-13.67-15.01-21.28c-7.59-13.55-11.09-28.35-13.43-43.53C12.72,136.37,12.85,132.64,12.66,128.88z'/>
</svg>

</span>
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@ applied.

The upstream mettermost version is specified in the `VERSION` file.
All patch files (files ending with .patch) in the root directory of the
repository are applied onto the specified mattermost version. When
adding new patches or changing them please make sure a description
exists and is up to date.
repository are applied onto the specified mattermost version. Files
inside of the `overlay/` directory are copied directly. When adding new
patches or changing them please make sure a description exists and is
up to date.

## License

The code is published under the AGPL version 3.0 only. See the
`LICENSE` file for details.
The code is published under the AGPL version 3.0 only unless otherwise
specified in the file. See the `LICENSE` file for details.
246 changes: 246 additions & 0 deletions overlay/server/channels/app/oauthproviders/openid/openid.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See [LICENSE.txt] for license information.
// [LICENSE.txt]: https://github.com/mattermost/mattermost/blob/03a5b4a288ba60c52e604e5ef017cf5fc179db51/LICENSE.txt

package oauthopenid

import (
"encoding/base64"
"encoding/json"
"errors"
"io"
"net/http"
"strconv"
"strings"
"time"

"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/public/shared/request"
"github.com/mattermost/mattermost/server/v8/einterfaces"
)

type CacheData struct {
Service string
Expires int64
Settings model.SSOSettings
}

type OpenIdMetadata struct {
Issuer string `json:"issuer"`
AuthorizationEndpoint string `json:"authorization_endpoint"`
TokenEndpoint string `json:"token_endpoint"`
UserEndpoint string `json:"userinfo_endpoint"`
JwksURI string `json:"jwks_uri"`
Algorithms []string `json:"id_token_signing_alg_values_supported"`
}

type OpenIdProvider struct {
CacheData *CacheData
}

type OpenIdUser struct {
Id string `json:"sub"`
Oid string `json:"oid"` //Office 365 only
FirstName string `json:"given_name"`
LastName string `json:"family_name"`
Name string `json:"name"`
Nickname string `json:"nickname"`
Email string `json:"email"`
}

func init() {
provider := &OpenIdProvider{}
einterfaces.RegisterOAuthProvider(model.ServiceOpenid, provider)
}

func (o *OpenIdProvider) userFromOpenIdUser(logger mlog.LoggerIFace, u *OpenIdUser) *model.User {
user := &model.User{}

user.Email = u.Email
user.Username = model.CleanUsername(logger, strings.Split(user.Email, "@")[0])
if o.CacheData.Service == model.ServiceGitlab && u.Nickname != "" {
user.Username = u.Nickname
}

user.FirstName = u.FirstName
user.LastName = u.LastName
user.Nickname = u.Nickname

user.AuthData = new(string)
*user.AuthData = o.getAuthData(u)

return user
}

func (o *OpenIdProvider) getAuthData(u *OpenIdUser) string {
if o.CacheData.Service == model.ServiceOffice365 {
if u.Oid != "" {
return u.Oid
}
}
return u.Id
}

func openIDUserFromJSON(data io.Reader) (*OpenIdUser, error) {
decoder := json.NewDecoder(data)
var u OpenIdUser
err := decoder.Decode(&u)
if err != nil {
return nil, err
}
return &u, nil
}

func (u *OpenIdUser) IsValid() error {
if u.Id == "" {
return errors.New("invalid id")
}

if u.Email == "" {
return errors.New("invalid emails")
}
return nil
}

func (u *OpenIdUser) GetIdentifier() string {
return model.ServiceOpenid
}

func (o *OpenIdProvider) GetUserFromJSON(c request.CTX, data io.Reader, tokenUser *model.User) (*model.User, error) {
oid, err := openIDUserFromJSON(data)
if err != nil {
return nil, err
}
jsonUser := o.userFromOpenIdUser(c.Logger(), oid)

if tokenUser != nil {
jsonUser = o.combineUsers(jsonUser, tokenUser)
}
return jsonUser, nil
}

func (o *OpenIdProvider) combineUsers(jsonUser *model.User, tokenUser *model.User) *model.User {
if o.CacheData.Service == model.ServiceOffice365 {
jsonUser.AuthData = tokenUser.AuthData
}
return jsonUser
}

func (o *OpenIdProvider) GetAuthDataFromJSON(data io.Reader) (string, error) {
u, err := openIDUserFromJSON(data)
if err != nil {
return "", err
}

err = u.IsValid()
if err != nil {
return "", err
}
return o.getAuthData(u), nil
}

// GetSSOSettings returns SSO Settings from Cache or Discovery Document
func (o *OpenIdProvider) GetSSOSettings(_ request.CTX, config *model.Config, service string) (*model.SSOSettings, error) {
settings := config.OpenIdSettings
if service == model.ServiceOffice365 {
settings = *config.Office365Settings.SSOSettings()
} else if service == model.ServiceGoogle {
settings = config.GoogleSettings
} else if service == model.ServiceGitlab {
settings = config.GitLabSettings
}

if o.CacheData != nil && !settingsChanged(*o.CacheData, service, settings) && o.CacheData.Expires > time.Now().Unix() {
return &o.CacheData.Settings, nil
}

var age int64 = 0
if *settings.DiscoveryEndpoint != "" {
response, err := http.Get(*settings.DiscoveryEndpoint)
if err != nil {
return nil, err
}
defer response.Body.Close()

for _, v := range strings.Split(response.Header.Get("Cache-Control"), ",") {
if strings.Contains(v, "max-age") {
ageValue := strings.Split(v, "=")[1]
age, _ = strconv.ParseInt(ageValue, 10, 64)
}
}
responseData, err := io.ReadAll(response.Body)
if err != nil {
return nil, err
}

var openIDResponse OpenIdMetadata
err = json.Unmarshal(responseData, &openIDResponse)
if err != nil {
return nil, err
}

settings.AuthEndpoint = &openIDResponse.AuthorizationEndpoint
settings.TokenEndpoint = &openIDResponse.TokenEndpoint
settings.UserAPIEndpoint = &openIDResponse.UserEndpoint
}
expires := time.Now().Unix() + age

o.CacheData = &CacheData{
Service: service,
Expires: expires,
Settings: settings,
}
return &settings, nil
}

func settingsChanged(cacheData CacheData, service string, configSettings model.SSOSettings) bool {
if cacheData.Service == service &&
cacheData.Settings.DiscoveryEndpoint == configSettings.DiscoveryEndpoint &&
cacheData.Settings.Secret == configSettings.Secret &&
cacheData.Settings.Id == configSettings.Id {
return false
}
return true
}

func (o *OpenIdProvider) GetUserFromIdToken(c request.CTX, idToken string) (*model.User, error) {
parts := strings.Split(idToken, ".")
if len(parts) != 3 {
return nil, errors.New("invalid Id Token")
}

b, err := base64.RawURLEncoding.DecodeString(parts[1])
if err != nil {
return nil, err
}

claims := &OpenIdUser{}
json.Unmarshal(b, &claims)

return o.userFromOpenIdUser(c.Logger(), claims), nil
}

func (o *OpenIdProvider) IsSameUser(_ request.CTX, dbUser, oauthUser *model.User) bool {
// Office365 OAuth would store Ids without dashes. (ie. 0e8fddd450d344999a93a390ee8cb83d)
// Office365 OpenId will return as a formatted GUID (ie. '0e8fddd4-50d3-4499-9a93-a390ee8cb83d')
// If this is a UUID that starts with all zero. (ie. 00000000-0000-0000-be95-fe607df5dbeb)
// For backwards compatibility we store the auth data from OAuth as be95fe607df5dbeb
if dbUser.AuthData == nil || oauthUser.AuthData == nil {
return false
}
dbID := *dbUser.AuthData
oauthID := *oauthUser.AuthData
if dbID == "" || oauthID == "" {
return false
}
parts := strings.Split(oauthID, "-")
for _, part := range parts {
if strings.Count(part, "0") != len(part) {
if !strings.Contains(dbID, part) {
return false
}
}
}
return true
}