// 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 ( "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 { endpoint := oauth2.Endpoint{ AuthURL: fmt.Sprintf("%s/adfs/oauth2/authorize", hostUrl), TokenURL: fmt.Sprintf("%s/adfs/oauth2/token", hostUrl), } 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"` } // GetToken // 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("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 } pToken := &AdfsToken{} err = json.Unmarshal(data, pToken) if err != nil { return nil, err } if pToken.ErrMsg != "" { return nil, fmt.Errorf(pToken.ErrMsg) } token := &oauth2.Token{ AccessToken: pToken.IdToken, Expiry: time.Unix(time.Now().Unix()+int64(pToken.ExpiresIn), 0), } return token, nil } // GetUserInfo // 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 } body, err := io.ReadAll(resp.Body) if err != nil { return nil, err } var respKeys struct { Keys []interface{} `json:"keys"` } if err := json.Unmarshal(body, &respKeys); err != nil { return nil, err } respKey, err := json.Marshal(&(respKeys.Keys[0])) if err != nil { return nil, err } keyset, err := jwk.ParseKey(respKey) if err != nil { return nil, err } tokenSrc := []byte(token.AccessToken) publicKey, _ := keyset.PublicKey() idToken, _ := jwt.Parse(tokenSrc, jwt.WithVerify(jwa.RS256, publicKey)) sid, _ := idToken.Get("sid") upn, _ := idToken.Get("upn") name, _ := idToken.Get("unique_name") userinfo := &UserInfo{ Id: sid.(string), Username: name.(string), DisplayName: name.(string), Email: upn.(string), } return userinfo, nil }