Skip to content

Commit

Permalink
feat: add Keycloak idp support (#356)
Browse files Browse the repository at this point in the history
* feat: add Keycloak idp support

Signed-off-by: Yixiang Zhao <seriouszyx@foxmail.com>

* fix: fix the profile UI

Signed-off-by: Yixiang Zhao <seriouszyx@foxmail.com>
  • Loading branch information
seriouszyx authored Dec 13, 2021
1 parent cf9e628 commit 4ca5f4b
Show file tree
Hide file tree
Showing 8 changed files with 88 additions and 22 deletions.
4 changes: 2 additions & 2 deletions controllers/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ func (c *ApiController) Login() {
userInfo := &idp.UserInfo{}
if provider.Category == "SAML" {
// SAML
userInfo.Id, err = object.ParseSamlResponse(form.SamlResponse)
userInfo.Id, err = object.ParseSamlResponse(form.SamlResponse, provider.Type)
if err != nil {
c.ResponseError(err.Error())
return
Expand Down Expand Up @@ -241,7 +241,7 @@ func (c *ApiController) Login() {
if form.Method == "signup" {
user := &object.User{}
if provider.Category == "SAML" {
user = object.GetUserByField(application.Organization, "id", userInfo.Id)
user = object.GetUser(fmt.Sprintf("%s/%s", application.Organization, userInfo.Id))
} else if provider.Category == "OAuth" {
user = object.GetUserByField(application.Organization, provider.Type, userInfo.Id)
if user == nil {
Expand Down
33 changes: 21 additions & 12 deletions object/saml.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ import (
dsig "github.com/russellhaering/goxmldsig"
)

func ParseSamlResponse(samlResponse string) (string, error) {
func ParseSamlResponse(samlResponse string, providerType string) (string, error) {
samlResponse, _ = url.QueryUnescape(samlResponse)
sp, err := buildSp(nil, samlResponse)
sp, err := buildSp(&Provider{Type: providerType}, samlResponse)
if err != nil {
return "", err
}
Expand Down Expand Up @@ -63,15 +63,8 @@ func buildSp(provider *Provider, samlResponse string) (*saml2.SAMLServiceProvide
origin := beego.AppConfig.String("origin")
certEncodedData := ""
if samlResponse != "" {
de, err := base64.StdEncoding.DecodeString(samlResponse)
if err != nil {
panic(err)
}
deStr := strings.Replace(string(de), "\n", "", -1)
res := regexp.MustCompile(`<ds:X509Certificate>(.*?)</ds:X509Certificate>`).FindAllStringSubmatch(deStr, -1)
str := res[0][0]
certEncodedData = str[20 : len(str)-21]
} else if provider != nil {
certEncodedData = parseSamlResponse(samlResponse, provider.Type)
} else if provider.IdP != "" {
certEncodedData = provider.IdP
}
certData, err := base64.StdEncoding.DecodeString(certEncodedData)
Expand All @@ -88,7 +81,7 @@ func buildSp(provider *Provider, samlResponse string) (*saml2.SAMLServiceProvide
AssertionConsumerServiceURL: fmt.Sprintf("%s/api/acs", origin),
IDPCertificateStore: &certStore,
}
if provider != nil {
if provider.Endpoint != "" {
randomKeyStore := dsig.RandomKeyStoreForTest()
sp.IdentityProviderSSOURL = provider.Endpoint
sp.IdentityProviderIssuer = provider.IssuerUrl
Expand All @@ -97,3 +90,19 @@ func buildSp(provider *Provider, samlResponse string) (*saml2.SAMLServiceProvide
}
return sp, nil
}

func parseSamlResponse(samlResponse string, providerType string) string {
de, err := base64.StdEncoding.DecodeString(samlResponse)
if err != nil {
panic(err)
}
deStr := strings.Replace(string(de), "\n", "", -1)
tagMap := map[string]string{
"Aliyun IDaaS": "ds",
"Keycloak": "dsig",
}
tag := tagMap[providerType]
expression := fmt.Sprintf("<%s:X509Certificate>([\\s\\S]*?)</%s:X509Certificate>", tag, tag)
res := regexp.MustCompile(expression).FindStringSubmatch(deStr)
return res[1]
}
1 change: 1 addition & 0 deletions web/src/ProviderEditPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ class ProviderEditPage extends React.Component {
} else if (provider.category === "SAML") {
return ([
{id: 'Aliyun IDaaS', name: 'Aliyun IDaaS'},
{id: 'Keycloak', name: 'Keycloak'},
]);
} else {
return [];
Expand Down
2 changes: 1 addition & 1 deletion web/src/Setting.js
Original file line number Diff line number Diff line change
Expand Up @@ -375,7 +375,7 @@ export function getClickable(text) {
}

export function getProviderLogo(provider) {
const idp = provider.type.toLowerCase();
const idp = provider.type.toLowerCase().trim().split(' ')[0];
const url = `${StaticBaseUrl}/img/social_${idp}.png`;
return (
<img width={30} height={30} src={url} alt={idp} />
Expand Down
9 changes: 8 additions & 1 deletion web/src/UserEditPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import SelectRegionBox from "./SelectRegionBox";

import {Controlled as CodeMirror} from 'react-codemirror2';
import "codemirror/lib/codemirror.css";
import SamlWidget from "./common/SamlWidget";
require('codemirror/theme/material-darker.css');
require("codemirror/mode/javascript/javascript");

Expand Down Expand Up @@ -302,7 +303,13 @@ class UserEditPage extends React.Component {
<div style={{marginBottom: 20}}>
{
(this.state.application === null || this.state.user === null) ? null : (
this.state.application?.providers.filter(providerItem => Setting.isProviderVisible(providerItem)).map((providerItem, index) => <OAuthWidget key={providerItem.name} labelSpan={(Setting.isMobile()) ? 10 : 3} user={this.state.user} application={this.state.application} providerItem={providerItem} onUnlinked={() => { return this.unlinked()}} />)
this.state.application?.providers.filter(providerItem => Setting.isProviderVisible(providerItem)).map((providerItem, index) =>
(providerItem.category === "OAuth") ? (
<OAuthWidget key={providerItem.name} labelSpan={(Setting.isMobile()) ? 10 : 3} user={this.state.user} application={this.state.application} providerItem={providerItem} onUnlinked={() => { return this.unlinked()}} />
) : (
<SamlWidget key={providerItem.name} labelSpan={(Setting.isMobile()) ? 10 : 3} user={this.state.user} application={this.state.application} providerItem={providerItem} onUnlinked={() => { return this.unlinked()}} />
)
)
)
}
</div>
Expand Down
12 changes: 6 additions & 6 deletions web/src/auth/LoginPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -194,14 +194,14 @@ class LoginPage extends React.Component {
return text;
}

getSamlUrl(providerId) {
getSamlUrl(provider) {
const params = new URLSearchParams(this.props.location.search);
let clientId = params.get("client_id")
let clientId = params.get("client_id");
let application = params.get("state");
let realRedirectUri = params.get("redirect_uri");
let redirectUri = `${window.location.origin}/callback/saml`
let providerName = providerId.split('/')[1];
AuthBackend.getSamlLogin(providerId).then((res) => {
let redirectUri = `${window.location.origin}/callback/saml`;
let providerName = provider.name;
AuthBackend.getSamlLogin(`${provider.owner}/${providerName}`).then((res) => {
const replyState = `${clientId}&${application}&${providerName}&${realRedirectUri}&${redirectUri}`;
window.location.href = `${res.data}&RelayState=${btoa(replyState)}`;
});
Expand All @@ -217,7 +217,7 @@ class LoginPage extends React.Component {
)
} else if (provider.category === "SAML") {
return (
<a key={provider.displayName} onClick={this.getSamlUrl.bind(this, provider.owner + "/" + provider.name)}>
<a key={provider.displayName} onClick={this.getSamlUrl.bind(this, provider)}>
<img width={width} height={width} src={Provider.getProviderLogo(provider)} alt={provider.displayName} style={{margin: margin}} />
</a>
)
Expand Down
4 changes: 4 additions & 0 deletions web/src/auth/Provider.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,10 @@ const otherProviderInfo = {
logo: `${StaticBaseUrl}/img/social_aliyun.png`,
url: "https://aliyun.com/product/idaas"
},
"Keycloak": {
logo: `${StaticBaseUrl}/img/social_keycloak.png`,
url: "https://www.keycloak.org/"
},
},
};

Expand Down
45 changes: 45 additions & 0 deletions web/src/common/SamlWidget.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import React from "react";
import {Col, Row} from "antd";
import * as Setting from "../Setting";

class SamlWidget extends React.Component {
constructor(props) {
super(props);
this.state = {
classes: props,
addressOptions: [],
affiliationOptions: [],
};
}

renderIdp(user, application, providerItem) {
const provider = providerItem.provider;
const name = user.name;

return (
<Row key={provider.name} style={{marginTop: '20px'}}>
<Col style={{marginTop: '5px'}} span={this.props.labelSpan}>
{
Setting.getProviderLogo(provider)
}
<span style={{marginLeft: '5px'}}>
{
`${provider.type}:`
}
</span>
</Col>
<Col span={24 - this.props.labelSpan} style={{marginTop: '5px'}}>
<span style={{
width: this.props.labelSpan === 3 ? '300px' : '130px',
display: (Setting.isMobile()) ? 'inline' : "inline-block"}}>{name}</span>
</Col>
</Row>
)
}

render() {
return this.renderIdp(this.props.user, this.props.application, this.props.providerItem)
}
}

export default SamlWidget;

0 comments on commit 4ca5f4b

Please sign in to comment.