13 "github.com/golang-jwt/jwt/v5"
14 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
15 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests"
18// DTTSSOProvider signs users in with the DTT SSO (auth-server).
19type DTTSSOProvider struct {
23var _ Provider = (*DTTSSOProvider)(nil)
26 dttSSOProviderName = "DTT SSO"
27 dttSSODefaultScope = "openid"
31 dttSSODefaultLoginURL = &url.URL{
33 Host: "auth.thebestagent.pro",
34 Path: "/oauth/authorize",
37 dttSSODefaultRedeemURL = &url.URL{
39 Host: "auth.thebestagent.pro",
43 dttSSODefaultValidateURL = &url.URL{
45 Host: "auth.thebestagent.pro",
46 Path: "/oauth/check_token",
50// NewDTTSSOProvider initiates a new DTTSSOProvider
51func NewDTTSSOProvider(p *ProviderData) *DTTSSOProvider {
52 p.setProviderDefaults(providerDefaults{
53 name: dttSSOProviderName,
54 loginURL: dttSSODefaultLoginURL,
55 redeemURL: dttSSODefaultRedeemURL,
57 validateURL: dttSSODefaultValidateURL,
58 scope: dttSSODefaultScope,
60 return &DTTSSOProvider{ProviderData: p}
63// dttSSOTokenResponse is the SSO's /oauth/token response.
64type dttSSOTokenResponse struct {
65 AccessToken string `json:"access_token"`
66 RefreshToken string `json:"refresh_token"`
67 ExpiresIn int64 `json:"expires_in"`
68 IDToken string `json:"id_token"`
71// dttSSOClaims holds the id_token claims the provider uses.
72type dttSSOClaims struct {
74 Email string `json:"email"`
75 Role string `json:"role"`
76 UserID string `json:"user_id"`
79// Redeem exchanges the authorization code for a session.
80func (p *DTTSSOProvider) Redeem(ctx context.Context, redirectURL, code, _ string) (*sessions.SessionState, error) {
82 return nil, ErrMissingCode
84 return p.requestTokens(ctx, url.Values{
85 "grant_type": {"authorization_code"},
87 "redirect_uri": {redirectURL},
91// RefreshSession renews the tokens and re-reads the email and role.
92func (p *DTTSSOProvider) RefreshSession(ctx context.Context, s *sessions.SessionState) (bool, error) {
93 if s == nil || s.RefreshToken == "" {
96 refreshed, err := p.requestTokens(ctx, url.Values{
97 "grant_type": {"refresh_token"},
98 "refresh_token": {s.RefreshToken},
101 return false, fmt.Errorf("unable to redeem refresh token: %v", err)
103 s.AccessToken = refreshed.AccessToken
104 s.RefreshToken = refreshed.RefreshToken
105 s.IDToken = refreshed.IDToken
106 s.Email = refreshed.Email
107 s.User = refreshed.User
108 s.Groups = refreshed.Groups
109 s.CreatedAt = refreshed.CreatedAt
110 s.ExpiresOn = refreshed.ExpiresOn
114// ValidateSession checks the access token with the SSO's check_token endpoint.
115func (p *DTTSSOProvider) ValidateSession(ctx context.Context, s *sessions.SessionState) bool {
116 return validateToken(ctx, p, s.AccessToken, makeOIDCHeader(s.AccessToken))
119// requestTokens calls the token endpoint and builds a session from the verified id_token.
121// The SSO compares the Basic credentials byte for byte, so they are sent unescaped
122// rather than through golang.org/x/oauth2, which URL-escapes them.
123func (p *DTTSSOProvider) requestTokens(ctx context.Context, params url.Values) (*sessions.SessionState, error) {
124 clientSecret, err := p.GetClientSecret()
128 credentials := base64.StdEncoding.EncodeToString([]byte(p.ClientID + ":" + clientSecret))
130 result := requests.New(p.RedeemURL.String()).
132 WithMethod(http.MethodPost).
133 WithBody(bytes.NewBufferString(params.Encode())).
134 SetHeader("Content-Type", "application/x-www-form-urlencoded").
135 SetHeader("Authorization", "Basic "+credentials).
137 if result.Error() != nil {
138 return nil, result.Error()
140 if result.StatusCode() != http.StatusOK {
141 return nil, fmt.Errorf("token request failed: status %d - %s", result.StatusCode(), result.Body())
144 var token dttSSOTokenResponse
145 if err := result.UnmarshalInto(&token); err != nil {
148 if token.AccessToken == "" {
149 return nil, errors.New("token response carried no access_token")
151 claims, err := p.verifyIDToken(token.IDToken, clientSecret)
156 s := &sessions.SessionState{
157 AccessToken: token.AccessToken,
158 RefreshToken: token.RefreshToken,
159 IDToken: token.IDToken,
163 if claims.Role != "" {
164 s.Groups = []string{claims.Role}
167 if token.ExpiresIn > 0 {
168 s.ExpiresIn(time.Duration(token.ExpiresIn) * time.Second)
173// verifyIDToken checks the id_token's HS256 signature (keyed on the client secret),
174// issuer, audience and expiry.
176// go-jose is not used: it rejects HMAC keys under 32 bytes, and SSO client secrets
178func (p *DTTSSOProvider) verifyIDToken(raw, clientSecret string) (*dttSSOClaims, error) {
180 return nil, errors.New("token response carried no id_token; the scope must be exactly \"openid\"")
182 var claims dttSSOClaims
183 _, err := jwt.ParseWithClaims(raw, &claims,
184 func(*jwt.Token) (any, error) { return []byte(clientSecret), nil },
185 jwt.WithValidMethods([]string{jwt.SigningMethodHS256.Alg()}),
186 jwt.WithIssuer(p.issuer()),
187 jwt.WithAudience(p.ClientID),
188 jwt.WithExpirationRequired(),
191 return nil, fmt.Errorf("verifying id_token: %v", err)
193 if claims.Email == "" {
194 return nil, errors.New("id_token carries no email")
199// issuer is the SSO's site host, the origin of the login URL.
200func (p *DTTSSOProvider) issuer() string {
201 return p.LoginURL.Scheme + "://" + p.LoginURL.Host
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list