Skip to content

Commit

Permalink
feat: add AD-FS support (casdoor#505)
Browse files Browse the repository at this point in the history
Signed-off-by: Steve0x2a <[email protected]>
  • Loading branch information
Steve0x2a authored Feb 20, 2022
1 parent 9cabc40 commit a33076a
Show file tree
Hide file tree
Showing 11 changed files with 201 additions and 103 deletions.
2 changes: 1 addition & 1 deletion controllers/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ func (c *ApiController) Login() {
clientSecret = provider.ClientSecret2
}

idProvider := idp.GetIdProvider(provider.Type, provider.SubType, clientId, clientSecret, provider.AppId, form.RedirectUri)
idProvider := idp.GetIdProvider(provider.Type, provider.SubType, clientId, clientSecret, provider.AppId, form.RedirectUri, provider.Domain)
if idProvider == nil {
c.ResponseError(fmt.Sprintf("The provider type: %s is not supported", provider.Type))
return
Expand Down
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ require (
github.com/golang-jwt/jwt/v4 v4.1.0
github.com/google/uuid v1.2.0
github.com/jinzhu/configor v1.2.1 // indirect
github.com/lestrrat-go/jwx v0.9.0
github.com/markbates/goth v1.68.1-0.20211006204042-9dc8905b41c8
github.com/qiangmzsx/string-adapter/v2 v2.1.0
github.com/qor/oss v0.0.0-20191031055114-aef9ba66bf76
Expand All @@ -25,7 +26,7 @@ require (
github.com/russellhaering/goxmldsig v1.1.1
github.com/satori/go.uuid v1.2.0 // indirect
github.com/smartystreets/goconvey v1.6.4 // indirect
github.com/stretchr/testify v1.6.1
github.com/stretchr/testify v1.7.0
github.com/tealeg/xlsx v1.0.5
github.com/thanhpk/randstr v1.0.4
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3
Expand Down
102 changes: 2 additions & 100 deletions go.sum

Large diffs are not rendered by default.

136 changes: 136 additions & 0 deletions idp/adfs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
// Copyright 2022 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package idp

import (
"bytes"
"crypto/tls"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"time"

"github.com/lestrrat-go/jwx/jwa"
"github.com/lestrrat-go/jwx/jwk"
"github.com/lestrrat-go/jwx/jwt"
"golang.org/x/oauth2"
)

type AdfsIdProvider struct {
Client *http.Client
Config *oauth2.Config
Host string
}

func NewAdfsIdProvider(clientId string, clientSecret string, redirectUrl string, hostUrl string) *AdfsIdProvider {
idp := &AdfsIdProvider{}

config := idp.getConfig(hostUrl)
config.ClientID = clientId
config.ClientSecret = clientSecret
config.RedirectURL = redirectUrl
idp.Config = config
idp.Host = hostUrl
return idp
}

func (idp *AdfsIdProvider) SetHttpClient(client *http.Client) {
tr := &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
}
idp.Client = client
idp.Client.Transport = tr
}

func (idp *AdfsIdProvider) getConfig(hostUrl string) *oauth2.Config {
var endpoint = oauth2.Endpoint{
AuthURL: fmt.Sprintf("%s/adfs/oauth2/authorize", hostUrl),
TokenURL: fmt.Sprintf("%s/adfs/oauth2/token", hostUrl),
}

var config = &oauth2.Config{
Endpoint: endpoint,
}

return config
}

type AdfsToken struct {
IdToken string `json:"id_token"`
ExpiresIn int `json:"expires_in"`
ErrMsg string `json:"error_description"`
}

// get more detail via: https://docs.microsoft.com/en-us/windows-server/identity/ad-fs/overview/ad-fs-openid-connect-oauth-flows-scenarios#request-an-access-token
func (idp *AdfsIdProvider) GetToken(code string) (*oauth2.Token, error) {
payload := url.Values{}
payload.Set("code", code)
payload.Set("grant_type", "authorization_code")
payload.Set("client_id", idp.Config.ClientID)
payload.Set("redirect_uri", idp.Config.RedirectURL)
resp, err := idp.Client.PostForm(idp.Config.Endpoint.TokenURL, payload)
if err != nil {
return nil, err
}
data, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}

pToken := &AdfsToken{}
err = json.Unmarshal(data, pToken)
if err != nil {
return nil, fmt.Errorf("fail to unmarshal token response: %s", err.Error())
}
if pToken.ErrMsg != "" {
return nil, fmt.Errorf("pToken.Errmsg = %s", pToken.ErrMsg)
}

token := &oauth2.Token{
AccessToken: pToken.IdToken,
Expiry: time.Unix(time.Now().Unix()+int64(pToken.ExpiresIn), 0),
}
return token, nil
}

// Since the userinfo endpoint of ADFS only returns sub,
// the id_token is used to resolve the userinfo
func (idp *AdfsIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error) {
resp, err := idp.Client.Get(fmt.Sprintf("%s/adfs/discovery/keys", idp.Host))
if err != nil {
return nil, err
}
keyset, err := jwk.Parse(resp.Body)
if err != nil {
return nil, err
}
tokenSrc := []byte(token.AccessToken)
publicKey, _ := keyset.Keys[0].Materialize()
id_token, _ := jwt.Parse(bytes.NewReader(tokenSrc), jwt.WithVerify(jwa.RS256, publicKey))
sid, _ := id_token.Get("sid")
upn, _ := id_token.Get("upn")
name, _ := id_token.Get("unique_name")
userinfo := &UserInfo{
Id: sid.(string),
Username: name.(string),
DisplayName: name.(string),
Email: upn.(string),
}
return userinfo, nil
}
4 changes: 3 additions & 1 deletion idp/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ type IdProvider interface {
GetUserInfo(token *oauth2.Token) (*UserInfo, error)
}

func GetIdProvider(typ string, subType string, clientId string, clientSecret string, appId string, redirectUrl string) IdProvider {
func GetIdProvider(typ string, subType string, clientId string, clientSecret string, appId string, redirectUrl string, hostUrl string) IdProvider {
if typ == "GitHub" {
return NewGithubIdProvider(clientId, clientSecret, redirectUrl)
} else if typ == "Google" {
Expand Down Expand Up @@ -66,6 +66,8 @@ func GetIdProvider(typ string, subType string, clientId string, clientSecret str
return NewLarkIdProvider(clientId, clientSecret, redirectUrl)
} else if typ == "GitLab" {
return NewGitlabIdProvider(clientId, clientSecret, redirectUrl)
} else if typ == "Adfs" {
return NewAdfsIdProvider(clientId, clientSecret, redirectUrl, hostUrl)
} else if typ == "Baidu" {
return NewBaiduIdProvider(clientId, clientSecret, redirectUrl)
} else if typ == "Infoflow" {
Expand Down
1 change: 1 addition & 0 deletions object/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ type User struct {
Wecom string `xorm:"wecom varchar(100)" json:"wecom"`
Lark string `xorm:"lark varchar(100)" json:"lark"`
Gitlab string `xorm:"gitlab varchar(100)" json:"gitlab"`
Adfs string `xorm:"adfs varchar(100)" json:"adfs"`
Baidu string `xorm:"baidu varchar(100)" json:"baidu"`
Infoflow string `xorm:"infoflow varchar(100)" json:"infoflow"`
Apple string `xorm:"apple varchar(100)" json:"apple"`
Expand Down
14 changes: 14 additions & 0 deletions web/src/ProviderEditPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,20 @@ class ProviderEditPage extends React.Component {
</React.Fragment>
)
}
{
this.state.provider.type !== "Adfs" ? null : (
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={2}>
{Setting.getLabel(i18next.t("provider:Domain"), i18next.t("provider:Domain - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.provider.domain} onChange={e => {
this.updateProviderField('domain', e.target.value);
}} />
</Col>
</Row>
)
}
{this.state.provider.category === "Storage" ? (
<div>
<Row style={{marginTop: '20px'}} >
Expand Down
1 change: 1 addition & 0 deletions web/src/Setting.js
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,7 @@ export function getProviderTypeOptions(category) {
{id: 'WeCom', name: 'WeCom'},
{id: 'Lark', name: 'Lark'},
{id: 'GitLab', name: 'GitLab'},
{id: 'Adfs', name: 'Adfs'},
{id: 'Baidu', name: 'Baidu'},
{id: 'Infoflow', name: 'Infoflow'},
{id: 'Apple', name: 'Apple'},
Expand Down
32 changes: 32 additions & 0 deletions web/src/auth/AdfsLoginButton.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright 2022 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import {createButton} from "react-social-login-buttons";
import {StaticBaseUrl} from "../Setting";

function Icon({ width = 24, height = 24, color }) {
return <img src={`${StaticBaseUrl}/buttons/adfs.svg`} alt="Sign in with ADFS" style={{width: 24, height: 24}} />;
}

const config = {
text: "Sign in with ADFS",
icon: Icon,
iconFormat: name => `fa fa-${name}`,
style: {background: "#ffffff", color: "#000000"},
activeStyle: {background: "#ededee"},
};

const AdfsLoginButton = createButton(config);

export default AdfsLoginButton;
3 changes: 3 additions & 0 deletions web/src/auth/LoginPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import LinkedInLoginButton from "./LinkedInLoginButton";
import WeComLoginButton from "./WeComLoginButton";
import LarkLoginButton from "./LarkLoginButton";
import GitLabLoginButton from "./GitLabLoginButton";
import AdfsLoginButton from "./AdfsLoginButton";
import BaiduLoginButton from "./BaiduLoginButton";
import InfoflowLoginButton from "./InfoflowLoginButton";
import AppleLoginButton from "./AppleLoginButton"
Expand Down Expand Up @@ -188,6 +189,8 @@ class LoginPage extends React.Component {
return <LarkLoginButton text={text} align={"center"} />
} else if (type === "GitLab") {
return <GitLabLoginButton text={text} align={"center"} />
} else if (type === "Adfs") {
return <AdfsLoginButton text={text} align={"center"} />
} else if (type === "Baidu") {
return <BaiduLoginButton text={text} align={"center"} />
} else if (type === "Infoflow") {
Expand Down
6 changes: 6 additions & 0 deletions web/src/auth/Provider.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ const authInfo = {
scope: "read_user+profile",
endpoint: "https://gitlab.com/oauth/authorize",
},
Adfs: {
scope: "openid",
endpoint: "http://example.com",
},
Baidu: {
scope: "basic",
endpoint: "http://openapi.baidu.com/oauth/2.0/authorize",
Expand Down Expand Up @@ -267,6 +271,8 @@ export function getAuthUrl(application, provider, method) {
return `${endpoint}?app_id=${provider.clientId}&redirect_uri=${redirectUri}&state=${state}`;
} else if (provider.type === "GitLab") {
return `${endpoint}?client_id=${provider.clientId}&redirect_uri=${redirectUri}&state=${state}&response_type=code&scope=${scope}`;
} else if (provider.type === "Adfs") {
return `${provider.domain}/adfs/oauth2/authorize?client_id=${provider.clientId}&redirect_uri=${redirectUri}&state=${state}&response_type=code&nonce=casdoor&scope=openid`;
} else if (provider.type === "Baidu") {
return `${endpoint}?client_id=${provider.clientId}&redirect_uri=${redirectUri}&state=${state}&response_type=code&scope=${scope}&display=popup`;
} else if (provider.type === "Infoflow"){
Expand Down

0 comments on commit a33076a

Please sign in to comment.