• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

go-pkgz / auth / 7255079117

18 Dec 2023 11:27PM UTC coverage: 82.941%. Remained the same
7255079117

Pull #189

github

web-flow
Bump golang.org/x/crypto from 0.14.0 to 0.17.0 in /_example

Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.14.0 to 0.17.0.
- [Commits](https://github.com/golang/crypto/compare/v0.14.0...v0.17.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Pull Request #189: Bump golang.org/x/crypto from 0.14.0 to 0.17.0 in /_example

2572 of 3101 relevant lines covered (82.94%)

6.93 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

77.01
/provider/apple.go
1
package provider
2

3
// Implementation sign in with Apple for allow users to sign in to web services using their Apple ID.
4
// For correct work this provider user must has Apple developer account and correct configure "sign in with Apple" at in
5
// See more: https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_rest_api
6
// and https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_js/incorporating_sign_in_with_apple_into_other_platforms
7

8
import (
9
        "context"
10
        "crypto"
11
        "crypto/ecdsa"
12
        "crypto/sha1"
13
        "crypto/x509"
14
        "encoding/json"
15
        "encoding/pem"
16
        "fmt"
17
        "io"
18
        "net/http"
19
        "net/url"
20
        "os"
21
        "strings"
22
        "time"
23

24
        "golang.org/x/oauth2"
25

26
        "github.com/go-pkgz/rest"
27
        "github.com/golang-jwt/jwt"
28

29
        "github.com/go-pkgz/auth/logger"
30
        "github.com/go-pkgz/auth/token"
31
)
32

33
const (
34
        // appleAuthURL is the base authentication URL for sign in with Apple ID and fetch request code for user validation request.
35
        appleAuthURL = "https://appleid.apple.com/auth/authorize"
36

37
        // appleTokenURL is the endpoint for verifying tokens and get user unique ID and E-mail
38
        appleTokenURL = "https://appleid.apple.com/auth/token" // #nosec
39

40
        // appleRequestContentType is the valid type which apple REST API accept only
41
        appleRequestContentType = "application/x-www-form-urlencoded"
42

43
        // UserAgent required to every request to Apple REST API
44
        defaultUserAgent = "github.com/go-pkgz/auth"
45

46
        // AcceptJSONHeader is the content to accept from response
47
        AcceptJSONHeader = "application/json"
48
)
49

50
// appleVerificationResponse is based on https://developer.apple.com/documentation/signinwithapplerestapi/tokenresponse
51
type appleVerificationResponse struct {
52
        // A token used to access allowed user data, but now not implemented public interface for it.
53
        AccessToken string `json:"access_token"`
54

55
        // Access token type, always equal the "bearer".
56
        TokenType string `json:"token_type"`
57

58
        // Access token expires time in seconds. Always equal 3600 seconds (1 hour)
59
        ExpiresIn int `json:"expires_in"`
60

61
        // The refresh token used to regenerate new access tokens.
62
        RefreshToken string `json:"refresh_token"`
63

64
        // Main JSON Web Token that contains the user’s identity information.
65
        IDToken string `json:"id_token"`
66

67
        // Used to capture any error returned in response. Always check error for empty
68
        Error string `json:"error"`
69
}
70

71
// AppleConfig is the main oauth2 required parameters for "Sign in with Apple"
72
type AppleConfig struct {
73
        ClientID     string // the identifier Services ID for your app created in Apple developer account.
74
        TeamID       string // developer Team ID (10 characters), required for create JWT. It available, after signed in at developer account, by link: https://developer.apple.com/account/#/membership
75
        KeyID        string // private key ID  assigned to private key obtain in Apple developer account
76
        ResponseMode string // changes method of receiving data in callback. Default value "form_post" (https://developer.apple.com/documentation/sign_in_with_apple/request_an_authorization_to_the_sign_in_with_apple_server?changes=_1_2#4066168)
77

78
        scopes       []string         // for this package allow only username scope and UID in token claims. Apple service API provide only "email" and "name" scope values (https://developer.apple.com/documentation/sign_in_with_apple/clientconfigi/3230955-scope)
79
        privateKey   interface{}      // private key from Apple obtained in developer account (the keys section). Required for create the Client Secret (https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens#3262048)
80
        publicKey    crypto.PublicKey // need for validate sign of token
81
        clientSecret string           // is the JWT client secret will create after first call and then used until expired
82
        jwkURL       string           // URL for fetch JWK Apple keys, need redefine for tests
83
}
84

85
// AppleHandler implements login via Apple ID
86
type AppleHandler struct {
87
        Params
88

89
        // all of these fields specific to particular oauth2 provider
90
        name string
91
        // infoURL  string not implemented at Apple side
92
        endpoint oauth2.Endpoint
93

94
        mapUser func(jwt.MapClaims) token.User // map info from InfoURL to User
95
        conf    AppleConfig                    // main config for Apple auth provider
96

97
        PrivateKeyLoader PrivateKeyLoaderInterface // custom function interface for load private key
98

99
}
100

101
// PrivateKeyLoaderInterface interface for implement custom loader for Apple private key from user source
102
type PrivateKeyLoaderInterface interface {
103
        LoadPrivateKey() ([]byte, error)
104
}
105

106
// LoadFromFileFunc is the type for use pre-defined private key loader function
107
// Path field must be set with actual path to private key file
108
type LoadFromFileFunc struct {
109
        Path string
110
}
111

112
// LoadApplePrivateKeyFromFile return instance for pre-defined loader function from local file
113
func LoadApplePrivateKeyFromFile(path string) LoadFromFileFunc {
5✔
114
        return LoadFromFileFunc{
5✔
115
                Path: path,
5✔
116
        }
5✔
117
}
5✔
118

119
// LoadPrivateKey implement pre-defined (built-in) PrivateKeyLoaderInterface interface method for load private key from local file
120
func (lf LoadFromFileFunc) LoadPrivateKey() ([]byte, error) {
2✔
121
        if lf.Path == "" {
2✔
122
                return nil, fmt.Errorf("empty private key path not allowed")
×
123
        }
×
124

125
        keyFile, err := os.Open(lf.Path)
2✔
126
        if err != nil {
2✔
127
                return nil, err
×
128
        }
×
129
        keyValue, err := io.ReadAll(keyFile)
2✔
130
        if err != nil {
2✔
131
                return nil, err
×
132
        }
×
133
        err = keyFile.Close()
2✔
134
        return keyValue, err
2✔
135
}
136

137
// NewApple create new AppleProvider instance with a user parameters
138
// Private key must be set, when instance create call, for create `client_secret`
139
func NewApple(p Params, appleCfg AppleConfig, privateKeyLoader PrivateKeyLoaderInterface) (*AppleHandler, error) {
16✔
140

16✔
141
        if p.L == nil {
32✔
142
                p.L = logger.NoOp
16✔
143
        }
16✔
144
        var emptyParams []string
16✔
145

16✔
146
        // check required parameters filled
16✔
147
        if appleCfg.ClientID == "" {
19✔
148
                emptyParams = append(emptyParams, "ClientID")
3✔
149
        }
3✔
150
        if appleCfg.TeamID == "" {
18✔
151
                emptyParams = append(emptyParams, "TeamID")
2✔
152
        }
2✔
153
        if appleCfg.KeyID == "" {
17✔
154
                emptyParams = append(emptyParams, "KeyID")
1✔
155
        }
1✔
156
        if len(emptyParams) > 0 {
19✔
157
                return nil, fmt.Errorf("required params missed: %s", strings.Join(emptyParams, ", "))
3✔
158
        }
3✔
159

160
        responseMode := "form_post"
13✔
161
        if appleCfg.ResponseMode != "" {
15✔
162
                responseMode = appleCfg.ResponseMode
2✔
163
        }
2✔
164

165
        ah := AppleHandler{
13✔
166
                Params: p,
13✔
167
                name:   "apple", // static name for an Apple provider
13✔
168

13✔
169
                conf: AppleConfig{
13✔
170
                        ClientID:     appleCfg.ClientID,
13✔
171
                        TeamID:       appleCfg.TeamID,
13✔
172
                        KeyID:        appleCfg.KeyID,
13✔
173
                        scopes:       []string{"name"},
13✔
174
                        jwkURL:       appleKeysURL,
13✔
175
                        ResponseMode: responseMode,
13✔
176
                },
13✔
177

13✔
178
                endpoint: oauth2.Endpoint{
13✔
179
                        AuthURL:  appleAuthURL,
13✔
180
                        TokenURL: appleTokenURL,
13✔
181
                },
13✔
182

13✔
183
                mapUser: func(claims jwt.MapClaims) token.User {
15✔
184
                        var usr token.User
2✔
185
                        if uid, ok := claims["sub"]; ok {
4✔
186
                                usr.ID = "apple_" + token.HashID(sha1.New(), uid.(string))
2✔
187
                        }
2✔
188
                        return usr
2✔
189
                },
190
        }
191

192
        if privateKeyLoader == nil {
14✔
193
                return nil, fmt.Errorf("private key loader undefined")
1✔
194
        }
1✔
195

196
        ah.PrivateKeyLoader = privateKeyLoader
12✔
197

12✔
198
        err := ah.initPrivateKey()
12✔
199
        return &ah, err
12✔
200
}
201

202
// initPrivateKey parse Apple private key and assign to AppleHandler
203
func (ah *AppleHandler) initPrivateKey() error {
12✔
204

12✔
205
        sKey, err := ah.PrivateKeyLoader.LoadPrivateKey()
12✔
206
        if err != nil {
12✔
207
                return fmt.Errorf("problem with private key loading: %w", err)
×
208
        }
×
209

210
        block, _ := pem.Decode(sKey)
12✔
211
        if block == nil {
12✔
212
                return fmt.Errorf("empty block after decoding")
×
213
        }
×
214
        ah.conf.privateKey, err = x509.ParsePKCS8PrivateKey(block.Bytes)
12✔
215
        if err != nil {
12✔
216
                return err
×
217
        }
×
218
        publicKey, ok := ah.conf.privateKey.(*ecdsa.PrivateKey)
12✔
219
        if !ok {
13✔
220
                return fmt.Errorf("provided private key is not ECDSA")
1✔
221
        }
1✔
222
        ah.conf.publicKey = publicKey.Public()
11✔
223
        ah.conf.clientSecret, err = ah.createClientSecret()
11✔
224
        if err != nil {
11✔
225
                return err
×
226
        }
×
227
        return nil
11✔
228
}
229

230
// tokenKeyFunc use for verify JWT sign, it receives the parsed token and should return the key for validating.
231
func (ah *AppleHandler) tokenKeyFunc(jwtToken *jwt.Token) (interface{}, error) {
6✔
232
        if jwtToken == nil {
6✔
233
                return nil, fmt.Errorf("failed to call token keyFunc, because token is nil")
×
234
        }
×
235
        return ah.conf.publicKey, nil // extract public key from private key
6✔
236
}
237

238
// Name of the provider
239
func (ah *AppleHandler) Name() string { return ah.name }
1✔
240

241
// LoginHandler - GET */{provider-name}/login
242
func (ah *AppleHandler) LoginHandler(w http.ResponseWriter, r *http.Request) {
1✔
243

1✔
244
        ah.Logf("[DEBUG] login with %s", ah.Name())
1✔
245
        // make state (random) and store in session
1✔
246
        state, err := randToken()
1✔
247
        if err != nil {
1✔
248
                rest.SendErrorJSON(w, r, ah.L, http.StatusInternalServerError, err, "failed to make oauth2 state")
×
249
                return
×
250
        }
×
251

252
        cid, err := randToken()
1✔
253
        if err != nil {
1✔
254
                rest.SendErrorJSON(w, r, ah.L, http.StatusInternalServerError, err, "failed to make claim's id")
×
255
                return
×
256
        }
×
257

258
        claims := token.Claims{
1✔
259
                Handshake: &token.Handshake{
1✔
260
                        State: state,
1✔
261
                        From:  r.URL.Query().Get("from"),
1✔
262
                },
1✔
263
                SessionOnly: r.URL.Query().Get("session") != "" && r.URL.Query().Get("session") != "0",
1✔
264
                StandardClaims: jwt.StandardClaims{
1✔
265
                        Id:        cid,
1✔
266
                        Audience:  r.URL.Query().Get("site"),
1✔
267
                        ExpiresAt: time.Now().Add(30 * time.Minute).Unix(),
1✔
268
                        NotBefore: time.Now().Add(-1 * time.Minute).Unix(),
1✔
269
                },
1✔
270
        }
1✔
271

1✔
272
        if _, err = ah.JwtService.Set(w, claims); err != nil {
1✔
273
                rest.SendErrorJSON(w, r, ah.L, http.StatusInternalServerError, err, "failed to set token")
×
274
                return
×
275
        }
×
276

277
        // return login url
278
        loginURL, err := ah.prepareLoginURL(state, r.URL.Path)
1✔
279
        if err != nil {
1✔
280
                errMsg := fmt.Sprintf("prepare login url for [%s] provider failed", ah.name)
×
281
                ah.Logf("[ERROR] %s", errMsg)
×
282
                rest.SendErrorJSON(w, r, ah.L, http.StatusInternalServerError, err, errMsg)
×
283
                return
×
284
        }
×
285
        ah.Logf("[DEBUG] login url %s, claims=%+v", loginURL, claims)
1✔
286

1✔
287
        http.Redirect(w, r, loginURL, http.StatusFound)
1✔
288
}
289

290
// AuthHandler fills user info and redirects to "from" url. This is callback url redirected locally by browser
291
// GET /callback
292
func (ah AppleHandler) AuthHandler(w http.ResponseWriter, r *http.Request) {
1✔
293

1✔
294
        // read response form data
1✔
295
        if err := r.ParseForm(); err != nil {
1✔
296
                rest.SendErrorJSON(w, r, ah.L, http.StatusInternalServerError, err, "read callback response from data failed")
×
297
                return
×
298
        }
×
299

300
        state := r.FormValue("state") // state value which sent with auth request
1✔
301
        code := r.FormValue("code")   //  client code for validation
1✔
302

1✔
303
        // response with user name filed return only one time at first login, next login  field user doesn't exist
1✔
304
        // until user delete sign with Apple ID in account profile (security section)
1✔
305
        // example response: {"name":{"firstName":"Chan","lastName":"Lu"},"email":"user@email.com"}
1✔
306
        jUser := r.FormValue("user") // json string with user name
1✔
307

1✔
308
        oauthClaims, _, err := ah.JwtService.Get(r)
1✔
309
        if err != nil {
1✔
310
                rest.SendErrorJSON(w, r, ah.L, http.StatusInternalServerError, err, "failed to get token")
×
311
                return
×
312
        }
×
313

314
        if oauthClaims.Handshake == nil {
1✔
315
                rest.SendErrorJSON(w, r, ah.L, http.StatusForbidden, nil, "invalid handshake token")
×
316
                return
×
317
        }
×
318

319
        retrievedState := oauthClaims.Handshake.State
1✔
320
        if retrievedState == "" || retrievedState != state {
1✔
321
                rest.SendErrorJSON(w, r, ah.L, http.StatusForbidden, nil, "unexpected state")
×
322
                return
×
323
        }
×
324

325
        var resp appleVerificationResponse
1✔
326
        err = ah.exchange(context.Background(), code, ah.makeRedirURL(r.URL.Path), &resp)
1✔
327
        if err != nil {
1✔
328
                rest.SendErrorJSON(w, r, ah.L, http.StatusInternalServerError, err, "exchange failed")
×
329
                return
×
330
        }
×
331
        ah.Logf("[DEBUG] response data %+v", resp)
1✔
332
        if resp.Error != "" {
1✔
333
                rest.SendErrorJSON(w, r, ah.L, http.StatusInternalServerError, nil, fmt.Sprintf("fetch IDtoken response error: %s", resp.Error))
×
334
                return
×
335
        }
×
336

337
        // trying to fetch Apple public key (JWK) for verify token signature, it need for verify IDToken received from Apple
338
        keySet, err := fetchAppleJWK(r.Context(), ah.conf.jwkURL)
1✔
339
        if err != nil {
1✔
340
                ah.L.Logf("[ERROR] failed to fetch JWK from Apple key service: " + err.Error())
×
341
                rest.SendErrorJSON(w, r, ah.L, http.StatusInternalServerError, nil, fmt.Sprintf("failed to fetch JWK from Apple key service: %s", resp.Error))
×
342
                return
×
343
        }
×
344

345
        // get token claims for extract uid (and email or name if they exist in scope)
346
        tokenClaims := jwt.MapClaims{}
1✔
347
        _, err = jwt.ParseWithClaims(resp.IDToken, tokenClaims, keySet.keyFunc)
1✔
348
        if err != nil {
1✔
349
                ah.L.Logf("[ERROR] failed to get claims: " + err.Error())
×
350
                rest.SendErrorJSON(w, r, ah.L, http.StatusInternalServerError, nil, fmt.Sprintf("failed to token validation, key is invalid: %s", resp.Error))
×
351
                return
×
352
        }
×
353

354
        u := ah.mapUser(tokenClaims)
1✔
355

1✔
356
        u, err = setAvatar(ah.AvatarSaver, u, &http.Client{Timeout: 5 * time.Second})
1✔
357
        if err != nil {
1✔
358
                rest.SendErrorJSON(w, r, ah.L, http.StatusInternalServerError, err, "failed to save avatar to proxy")
×
359
                return
×
360
        }
×
361

362
        // try parse username if one exist at response or noname assign
363
        ah.parseUserData(&u, jUser)
1✔
364

1✔
365
        cid, err := randToken()
1✔
366
        if err != nil {
1✔
367
                rest.SendErrorJSON(w, r, ah.L, http.StatusInternalServerError, err, "failed to make claim's id")
×
368
                return
×
369
        }
×
370

371
        claims := token.Claims{
1✔
372
                User: &u,
1✔
373
                StandardClaims: jwt.StandardClaims{
1✔
374
                        Issuer:   ah.Issuer,
1✔
375
                        Id:       cid,
1✔
376
                        Audience: oauthClaims.Audience,
1✔
377
                },
1✔
378
                SessionOnly: false,
1✔
379
        }
1✔
380

1✔
381
        if _, err = ah.JwtService.Set(w, claims); err != nil {
1✔
382
                rest.SendErrorJSON(w, r, ah.L, http.StatusInternalServerError, err, "failed to set token")
×
383
                return
×
384
        }
×
385

386
        ah.Logf("[DEBUG] user info %+v", u)
1✔
387

1✔
388
        // redirect to back url if presented in login query params
1✔
389
        if oauthClaims.Handshake != nil && oauthClaims.Handshake.From != "" {
1✔
390
                http.Redirect(w, r, oauthClaims.Handshake.From, http.StatusTemporaryRedirect)
×
391
                return
×
392
        }
×
393
        rest.RenderJSON(w, &u)
1✔
394

395
}
396

397
// LogoutHandler - GET /logout
398
func (ah AppleHandler) LogoutHandler(w http.ResponseWriter, r *http.Request) {
2✔
399
        if _, _, err := ah.JwtService.Get(r); err != nil {
3✔
400
                rest.SendErrorJSON(w, r, ah.L, http.StatusForbidden, err, "logout not allowed")
1✔
401
                return
1✔
402
        }
1✔
403
        ah.JwtService.Reset(w)
1✔
404
}
405

406
// exchange sends the validation token request and gets access token and user claims
407
// (e.g. https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens)
408
func (ah *AppleHandler) exchange(ctx context.Context, code, redirectURI string, result *appleVerificationResponse) error {
4✔
409

4✔
410
        // check client_secret for valid and recreate new (client_secret JWT) if required
4✔
411
        if tkn, err := jwt.Parse(ah.conf.clientSecret, ah.tokenKeyFunc); err != nil || tkn == nil {
4✔
412
                ah.conf.clientSecret, err = ah.createClientSecret()
×
413
                if err != nil {
×
414
                        return fmt.Errorf("client secret create failed: %w", err)
×
415
                }
×
416
        }
417

418
        data := url.Values{}
4✔
419
        data.Set("client_id", ah.conf.ClientID)
4✔
420
        data.Set("client_secret", ah.conf.clientSecret) // JWT signed with Apple private key
4✔
421
        data.Set("code", code)
4✔
422
        data.Set("redirect_uri", redirectURI) // redirect URL can't refer to localhost and must have trusted certificate and https protocol
4✔
423
        data.Set("grant_type", "authorization_code")
4✔
424

4✔
425
        client := http.Client{Timeout: time.Second * 5}
4✔
426
        req, err := http.NewRequestWithContext(ctx, "POST", ah.endpoint.TokenURL, strings.NewReader(data.Encode()))
4✔
427
        if err != nil {
4✔
428
                return err
×
429
        }
×
430

431
        req.Header.Add("content-type", appleRequestContentType)
4✔
432
        req.Header.Add("accept", AcceptJSONHeader)
4✔
433
        req.Header.Add("user-agent", defaultUserAgent) // apple requires a user agent
4✔
434

4✔
435
        res, err := client.Do(req)
4✔
436
        if err != nil {
4✔
437
                return err
×
438
        }
×
439

440
        // Trying to decode (unmarshal json) data of response
441
        err = json.NewDecoder(res.Body).Decode(result)
4✔
442
        if err != nil {
5✔
443
                return fmt.Errorf("unmarshalling data from apple service response failed: %w", err)
1✔
444
        }
1✔
445

446
        defer func() {
6✔
447
                if err = res.Body.Close(); err != nil {
3✔
448
                        ah.L.Logf("[ERROR] close request body failed when get access token: %v", err)
×
449
                }
×
450
        }()
451

452
        // If above operation done successfully checking a response code and error descriptions, if one exist.
453
        // Apple service will response either 200 (OK) or 400 (any error).
454
        if res.StatusCode != http.StatusOK || result.Error != "" {
4✔
455
                return fmt.Errorf("apple token service error: %s", result.Error)
1✔
456
        }
1✔
457

458
        return err
2✔
459
}
460

461
// createClientSecret use for create the JWT client secret required to make requests to the Apple validation server.
462
// for more details go to link: https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens#3262048
463
func (ah *AppleHandler) createClientSecret() (string, error) {
13✔
464

13✔
465
        if ah.conf.privateKey == nil {
14✔
466
                return "", fmt.Errorf("private key can't be empty")
1✔
467
        }
1✔
468
        // Create a claims
469
        now := time.Now()
12✔
470
        exp := now.Add(time.Minute * 30).Unix() // default value
12✔
471

12✔
472
        claims := &jwt.StandardClaims{
12✔
473
                Issuer:    ah.conf.TeamID,
12✔
474
                IssuedAt:  now.Unix(),
12✔
475
                ExpiresAt: exp,
12✔
476
                Audience:  "https://appleid.apple.com",
12✔
477
                Subject:   ah.conf.ClientID,
12✔
478
        }
12✔
479

12✔
480
        tkn := jwt.NewWithClaims(jwt.SigningMethodES256, claims)
12✔
481
        tkn.Header["alg"] = "ES256"
12✔
482
        tkn.Header["kid"] = ah.conf.KeyID
12✔
483

12✔
484
        return tkn.SignedString(ah.conf.privateKey)
12✔
485
}
486

487
func (ah *AppleHandler) parseUserData(user *token.User, jUser string) {
3✔
488

3✔
489
        type UserData struct {
3✔
490
                Name struct {
3✔
491
                        FirstName string `json:"firstName"`
3✔
492
                        LastName  string `json:"lastName"`
3✔
493
                } `json:"name"`
3✔
494
                Email string `json:"email"`
3✔
495
        }
3✔
496

3✔
497
        var userData UserData
3✔
498

3✔
499
        // Catch error for log only. No need break flow if user name doesn't exist
3✔
500
        if err := json.Unmarshal([]byte(jUser), &userData); err != nil {
5✔
501
                ah.L.Logf("[DEBUG] failed to parse user data %s: %v", user, err)
2✔
502
                user.Name = "noname_" + user.ID[6:12] // paste noname if user name failed to parse
2✔
503
                return
2✔
504
        }
2✔
505

506
        user.Name = fmt.Sprintf("%s %s", userData.Name.FirstName, userData.Name.LastName)
1✔
507
}
508

509
func (ah *AppleHandler) prepareLoginURL(state, path string) (string, error) {
4✔
510

4✔
511
        scopesList := strings.Join(ah.conf.scopes, " ")
4✔
512

4✔
513
        if scopesList != "" && ah.conf.ResponseMode != "form_post" {
5✔
514
                return "", fmt.Errorf("response_mode must be form_post if scope is not empty")
1✔
515
        }
1✔
516

517
        authURL, err := url.Parse(ah.endpoint.AuthURL)
3✔
518
        if err != nil {
3✔
519
                return "", err
×
520
        }
×
521

522
        query := authURL.Query()
3✔
523
        query.Set("state", state)
3✔
524
        query.Set("response_type", "code")
3✔
525
        query.Set("response_mode", ah.conf.ResponseMode)
3✔
526
        query.Set("client_id", ah.conf.ClientID)
3✔
527
        query.Set("scope", scopesList)
3✔
528
        query.Set("redirect_uri", ah.makeRedirURL(path))
3✔
529
        authURL.RawQuery = query.Encode()
3✔
530

3✔
531
        return authURL.String(), nil
3✔
532

533
}
534

535
func (ah AppleHandler) makeRedirURL(path string) string {
10✔
536
        elems := strings.Split(path, "/")
10✔
537
        newPath := strings.Join(elems[:len(elems)-1], "/")
10✔
538

10✔
539
        return strings.TrimRight(ah.URL, "/") + strings.TrimSuffix(newPath, "/") + urlCallbackSuffix
10✔
540
}
10✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc