Skip to content

Commit

Permalink
Add new B2C provider
Browse files Browse the repository at this point in the history
  • Loading branch information
nomeguy committed Dec 17, 2023
1 parent 468ceb6 commit 81b8d5e
Show file tree
Hide file tree
Showing 11 changed files with 184 additions and 8 deletions.
6 changes: 4 additions & 2 deletions idp/adfs.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,12 @@ func (idp *AdfsIdProvider) GetToken(code string) (*oauth2.Token, error) {
payload.Set("client_id", idp.Config.ClientID)
payload.Set("client_secret", idp.Config.ClientSecret)
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
Expand All @@ -97,10 +99,10 @@ func (idp *AdfsIdProvider) GetToken(code string) (*oauth2.Token, error) {
pToken := &AdfsToken{}
err = json.Unmarshal(data, pToken)
if err != nil {
return nil, fmt.Errorf("fail to unmarshal token response: %s", err.Error())
return nil, err
}
if pToken.ErrMsg != "" {
return nil, fmt.Errorf("pToken.Errmsg = %s", pToken.ErrMsg)
return nil, fmt.Errorf(pToken.ErrMsg)
}

token := &oauth2.Token{
Expand Down
126 changes: 126 additions & 0 deletions idp/azuread_b2c.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
// Copyright 2023 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 (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"time"

"golang.org/x/oauth2"
)

type AzureADB2CProvider struct {
Client *http.Client
Config *oauth2.Config
Tenant string
UserFlow string
}

func NewAzureAdB2cProvider(clientId, clientSecret, redirectUrl, tenant string, userFlow string) *AzureADB2CProvider {
return &AzureADB2CProvider{
Config: &oauth2.Config{
ClientID: clientId,
ClientSecret: clientSecret,
RedirectURL: redirectUrl,
Endpoint: oauth2.Endpoint{
AuthURL: fmt.Sprintf("https://%s.b2clogin.com/%s.onmicrosoft.com/%s/oauth2/v2.0/authorize", tenant, tenant, userFlow),
TokenURL: fmt.Sprintf("https://%s.b2clogin.com/%s.onmicrosoft.com/%s/oauth2/v2.0/token", tenant, tenant, userFlow),
},
Scopes: []string{"openid", "email"},
},
Tenant: tenant,
UserFlow: userFlow,
}
}

func (p *AzureADB2CProvider) SetHttpClient(client *http.Client) {
p.Client = client
}

type AzureadB2cToken struct {
IdToken string `json:"id_token"`
TokenType string `json:"token_type"`
NotBefore int `json:"not_before"`
IdTokenExpiresIn int `json:"id_token_expires_in"`
ProfileInfo string `json:"profile_info"`
Scope string `json:"scope"`
}

func (p *AzureADB2CProvider) GetToken(code string) (*oauth2.Token, error) {
payload := url.Values{}
payload.Set("code", code)
payload.Set("grant_type", "authorization_code")
payload.Set("client_id", p.Config.ClientID)
payload.Set("client_secret", p.Config.ClientSecret)
payload.Set("redirect_uri", p.Config.RedirectURL)

resp, err := p.Client.PostForm(p.Config.Endpoint.TokenURL, payload)
if err != nil {
return nil, err
}

data, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}

pToken := &AzureadB2cToken{}
err = json.Unmarshal(data, pToken)
if err != nil {
return nil, err
}

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

func (p *AzureADB2CProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error) {
userInfoEndpoint := fmt.Sprintf("https://%s.b2clogin.com/%s.onmicrosoft.com/%s/openid/v2.0/userinfo", p.Tenant, p.Tenant, p.UserFlow)
req, err := http.NewRequest("GET", userInfoEndpoint, nil)
if err != nil {
return nil, err
}
req.Header.Add("Authorization", "Bearer "+token.AccessToken)

resp, err := p.Client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("error fetching user info: status code %d", resp.StatusCode)
}

bodyBytes, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}

var userInfo UserInfo
err = json.Unmarshal(bodyBytes, &userInfo)
if err != nil {
return nil, err
}

return &userInfo, nil
}
2 changes: 2 additions & 0 deletions idp/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ func GetIdProvider(idpInfo *ProviderInfo, redirectUrl string) (IdProvider, error
return NewGitlabIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl), nil
case "ADFS":
return NewAdfsIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl, idpInfo.HostUrl), nil
case "AzureADB2C":
return NewAzureAdB2cProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl, idpInfo.HostUrl, idpInfo.AppId), nil
case "Baidu":
return NewBaiduIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl), nil
case "Alipay":
Expand Down
2 changes: 1 addition & 1 deletion object/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -417,7 +417,7 @@ func FromProviderToIdpInfo(ctx *context.Context, provider *Provider) *idp.Provid
providerInfo.ClientId = provider.ClientId2
providerInfo.ClientSecret = provider.ClientSecret2
}
} else if provider.Type == "AzureAD" || provider.Type == "ADFS" || provider.Type == "Okta" {
} else if provider.Type == "AzureAD" || provider.Type == "AzureADB2C" || provider.Type == "ADFS" || provider.Type == "Okta" {
providerInfo.HostUrl = provider.Domain
}

Expand Down
3 changes: 2 additions & 1 deletion object/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ type User struct {
Infoflow string `xorm:"infoflow varchar(100)" json:"infoflow"`
Apple string `xorm:"apple varchar(100)" json:"apple"`
AzureAD string `xorm:"azuread varchar(100)" json:"azuread"`
AzureADB2c string `xorm:"azuread varchar(100)" json:"azureadb2c"`
Slack string `xorm:"slack varchar(100)" json:"slack"`
Steam string `xorm:"steam varchar(100)" json:"steam"`
Bilibili string `xorm:"bilibili varchar(100)" json:"bilibili"`
Expand Down Expand Up @@ -622,7 +623,7 @@ func UpdateUser(id string, user *User, columns []string, isAdmin bool) (bool, er
"is_admin", "is_forbidden", "is_deleted", "hash", "is_default_avatar", "properties", "webauthnCredentials", "managedAccounts",
"signin_wrong_times", "last_signin_wrong_time", "groups", "access_key", "access_secret",
"github", "google", "qq", "wechat", "facebook", "dingtalk", "weibo", "gitee", "linkedin", "wecom", "lark", "gitlab", "adfs",
"baidu", "alipay", "casdoor", "infoflow", "apple", "azuread", "slack", "steam", "bilibili", "okta", "douyin", "line", "amazon",
"baidu", "alipay", "casdoor", "infoflow", "apple", "azuread", "azureadb2c", "slack", "steam", "bilibili", "okta", "douyin", "line", "amazon",
"auth0", "battlenet", "bitbucket", "box", "cloudfoundry", "dailymotion", "deezer", "digitalocean", "discord", "dropbox",
"eveonline", "fitbit", "gitea", "heroku", "influxcloud", "instagram", "intercom", "kakao", "lastfm", "mailru", "meetup",
"microsoftonline", "naver", "nextcloud", "onedrive", "oura", "patreon", "paypal", "salesforce", "shopify", "soundcloud",
Expand Down
5 changes: 4 additions & 1 deletion web/src/ProviderEditPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,9 @@ class ProviderEditPage extends React.Component {
} else if (provider.type === "Infoflow") {
text = i18next.t("provider:Agent ID");
tooltip = i18next.t("provider:Agent ID - Tooltip");
} else if (provider.type === "AzureADB2C") {
text = i18next.t("provider:User flow");
tooltip = i18next.t("provider:User flow - Tooltip");
}
} else if (provider.category === "SMS") {
if (provider.type === "Twilio SMS" || provider.type === "Azure ACS") {
Expand Down Expand Up @@ -758,7 +761,7 @@ class ProviderEditPage extends React.Component {
)
}
{
this.state.provider.type !== "ADFS" && this.state.provider.type !== "AzureAD" && this.state.provider.type !== "Casdoor" && this.state.provider.type !== "Okta" ? null : (
this.state.provider.type !== "ADFS" && this.state.provider.type !== "AzureAD" && this.state.provider.type !== "AzureADB2C" && this.state.provider.type !== "Casdoor" && this.state.provider.type !== "Okta" ? null : (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{Setting.getLabel(i18next.t("provider:Domain"), i18next.t("provider:Domain - Tooltip"))} :
Expand Down
3 changes: 2 additions & 1 deletion web/src/Setting.js
Original file line number Diff line number Diff line change
Expand Up @@ -924,7 +924,8 @@ export function getProviderTypeOptions(category) {
{id: "Casdoor", name: "Casdoor"},
{id: "Infoflow", name: "Infoflow"},
{id: "Apple", name: "Apple"},
{id: "AzureAD", name: "AzureAD"},
{id: "AzureAD", name: "Azure AD"},
{id: "AzureADB2C", name: "Azure AD B2C"},
{id: "Slack", name: "Slack"},
{id: "Steam", name: "Steam"},
{id: "Bilibili", name: "Bilibili"},
Expand Down
32 changes: 32 additions & 0 deletions web/src/auth/AzureADB2CLoginButton.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright 2023 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/azuread.svg`} alt="Sign in with Azure AD B2C" style={{width: 24, height: 24}} />;
}

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

const AzureADB2CLoginButton = createButton(config);

export default AzureADB2CLoginButton;
4 changes: 2 additions & 2 deletions web/src/auth/AzureADLoginButton.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@ import {createButton} from "react-social-login-buttons";
import {StaticBaseUrl} from "../Setting";

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

const config = {
text: "Sign in with AzureAD",
text: "Sign in with Azure AD",
icon: Icon,
iconFormat: name => `fa fa-${name}`,
style: {background: "#ffffff", color: "#000000"},
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 @@ -100,6 +100,10 @@ const authInfo = {
scope: "user.read",
endpoint: "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
},
AzureADB2C: {
scope: "openid",
endpoint: "https://tenant.b2clogin.com/tenant.onmicrosoft.com/userflow/oauth2/v2.0/authorize",
},
Slack: {
scope: "users:read",
endpoint: "https://slack.com/oauth/authorize",
Expand Down Expand Up @@ -406,6 +410,8 @@ export function getAuthUrl(application, provider, method) {
|| provider.type === "Twitch" || provider.type === "Typetalk" || provider.type === "Uber" || provider.type === "VK" || provider.type === "Wepay"
|| provider.type === "Xero" || provider.type === "Yahoo" || provider.type === "Yammer" || provider.type === "Yandex" || provider.type === "Zoom") {
return `${endpoint}?client_id=${provider.clientId}&redirect_uri=${redirectUri}&scope=${scope}&response_type=code&state=${state}`;
} else if (provider.type === "AzureADB2C") {
return `https://${provider.domain}.b2clogin.com/${provider.domain}.onmicrosoft.com/${provider.appId}/oauth2/v2.0/authorize?client_id=${provider.clientId}&nonce=defaultNonce&redirect_uri=${encodeURIComponent(redirectUri)}&scope=${scope}&response_type=code&state=${state}&prompt=login`;
} else if (provider.type === "DingTalk") {
return `${endpoint}?client_id=${provider.clientId}&redirect_uri=${redirectUri}&scope=${scope}&response_type=code&prompt=consent&state=${state}`;
} else if (provider.type === "WeChat") {
Expand Down
3 changes: 3 additions & 0 deletions web/src/auth/ProviderButton.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import AlipayLoginButton from "./AlipayLoginButton";
import InfoflowLoginButton from "./InfoflowLoginButton";
import AppleLoginButton from "./AppleLoginButton";
import AzureADLoginButton from "./AzureADLoginButton";
import AzureADB2CLoginButton from "./AzureADB2CLoginButton";
import SlackLoginButton from "./SlackLoginButton";
import SteamLoginButton from "./SteamLoginButton";
import BilibiliLoginButton from "./BilibiliLoginButton";
Expand Down Expand Up @@ -85,6 +86,8 @@ function getSigninButton(provider) {
return <AppleLoginButton text={text} align={"center"} />;
} else if (provider.type === "AzureAD") {
return <AzureADLoginButton text={text} align={"center"} />;
} else if (provider.type === "AzureADB2C") {
return <AzureADB2CLoginButton text={text} align={"center"} />;
} else if (provider.type === "Slack") {
return <SlackLoginButton text={text} align={"center"} />;
} else if (provider.type === "Steam") {
Expand Down

0 comments on commit 81b8d5e

Please sign in to comment.