siwago

package module
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 3, 2022 License: MIT Imports: 17 Imported by: 0

README

Siwago

Siwago is an implementation of authenticating "Sign in with Apple" tokens with Apple servers. It implements the validation and generation of tokens outlined at this link: https://developer.apple.com/documentation/signinwithapplerestapi/generate_and_validate_tokens

This repo contains the functionality to validate the authorization code provided to the client while signing in with Apple and to generate access_token. This is intended to be used on the server side.

This implementation has no external dependencies and manages encoding and singing using the go provided core functions.

Install & Usage

Install

Execute go get github.com/pagnihotry/siwago. This installs the package in your $GOPATH.

Usage

Get the tokens from Apple in the following three steps:

  1. Initialize SiwaConfig object siwago.GetObject(KID, TEAMID, BUNDLEID, duration, nonce)
  2. Set the private key in one of the following ways
    • Using file: siwagoObj.SetSecretP8File("/etc/keys/AuthKey_3UHT5POLK9.p8")
    • Using file contents as string: siwagoObj.SetSecretP8String("//-----BEGIN PRIVATE KEY-----\njkfweshjdjkhjsbjvguybjebvuewkvbbhj+jbdhbjhbvjhbvjhbvbjvbvjvagcve\njkfweshjdjkhjsbjvguybje/vuewkvbbhjdjbdhbjhbvjhbvjhbvbjvbvjvagcve\njkfweshjdjkhjsbjvguybjebvuewkvbbhj+jbdhbjhbvjhbvjhbvbjvbvjvagcve\njkfweshj\n-----END PRIVATE KEY-----")
    • Using file contents as []byte: siwagoObj.SetSecretP8Bytes(byteContents)
  3. Exchange the code for a token token:=siwagoObj.ExchangeAuthCode(code, redirectUri) redirectUri can be "" if it does not apply

If there is an error token.Error is set to the error recieved from Apple. More info: https://developer.apple.com/documentation/signinwithapplerestapi/errorresponse

In case of success token object is populated with the access token, refresh token, etc. from apple. More info: https://developer.apple.com/documentation/signinwithapplerestapi/tokenresponse

Details

SiwaConfig object

This object holds the app configuration that will be used to generate JWT token. This token can be initialized using a helper function GetObject(keyId string, teamId string, bundleId string, d time.Duration) or directly SiwaConfig{KeyId: keyId, TokenDelta: d, TeamId: teamId, BundleId: bundleId}

type SiwaConfig struct {
    //key Id from Certificates, Identifiers & Profiles on developers.apple.com
    KeyId           string 
    //duration for which you would want the generated client_secret jwt token to be valid.
    //Can not be more than 15777000 (6 months in seconds) from the Current Unix Time on the server.
    TokenDelta      time.Duration 
    //Team Id that is configured with Key, 
    //can also ne found in Certificates, Identifiers & Profiles on developers.apple.com
    TeamId          string 
    //bundleId for product com.companyname.product
    BundleId        string 
    //contents of the p8 file
    PemFileContents []byte 
    //nonce is set while making the request to generate authorization_code. If you dont use it, keep it an empty string
    Nonce           string
}
Setting Secret

The signing secret or the private key can be set with a file on the disk - .p8 file downloaded from the Apple Developer website, or as a string or []byte.

Private key file format:

-----BEGIN PRIVATE KEY-----
jkfweshjdjkhjsbjvguybjebvuewkvbbhj+jbdhbjhbvjhbvjhbvbjvbvjvagcve
jkfweshjdjkhjsbjvguybje/vuewkvbbhjdjbdhbjhbvjhbvjhbvbjvbvjvagcve
jkfweshjdjkhjsbjvguybjebvuewkvbbhj+jbdhbjhbvjhbvjhbvbjvbvjvagcve
jkfweshj
-----END PRIVATE KEY-----
File
func (self *SiwaConfig) SetSecretP8File(p8Filename string) error
string
func (self *SiwaConfig) SetSecretP8String(p8Contents string)
[]byte
func (self *SiwaConfig) SetSecretP8Bytes(p8Contents []byte)
Token object

The token object is returned by the ExchangeAuthCode method. This contains the response from Apple. The API returns a TokenResponse or ErrorResponse. If there is an error, the Token.Error is set with the message, else all the other fields are populated.

To check for errors, see if .Error is "".

type Token struct {
    //(Reserved for future use) A token used to access allowed data. Currently, no data set has been defined for access.
    AccessToken  string `json:"access_token"`
    //The type of access token. It will always be bearer.
    TokenType    string `json:"token_type"`
    //The amount of time, in seconds, before the access token expires.
    ExpiresIn    int64  `json:"expires_in"`
    //The refresh token used to regenerate new access tokens. Store this token securely on your server.
    RefreshToken string `json:"refresh_token"`
    //A JSON Web Token that contains the user’s identity information.
    IdToken      string `json:"id_token"`
    //Set if ErrorResponse is recieved
    //A string that describes the reason for the unsuccessful request. The string consists of a single allowed value.
    //Possible values: invalid_request, invalid_client, invalid_grant, unauthorized_client, unsupported_grant_type, invalid_scope
    Error        string `json:"error"`
    //After the token is fetched from apple, id token is validated
    //this field stores the result of the validation check
    Valid bool `json:"_"`
    //The decoded Id token
    //Holds the decoded JWT Header, Body, Signature and result of validity check
    DecodedIdToken *SiwaIdToken `json:"_"`
}
Token Validation

The id_token returned by apple is validated as follows:

  • Verify the JWS E256 signature using the server’s public key
  • Verify the nonce for the authentication
  • Verify that the iss field contains https://appleid.apple.com
  • Verify that the aud field is the developer’s client_id
  • Verify that the time is earlier than the exp value of the token

This is as per the guidelines on apple developer website at https://developer.apple.com/documentation/signinwithapplerestapi/verifying_a_user

Sample code

Here is a sample implementation for checking the token

package main

import (
    "fmt"
    "time"

    "github.com/pagnihotry/siwago"
)

const KID = "3UHT5POLK9"
const BUNDLEID = "com.company.product"
const TEAMID = "JSFD9L6MCB"

func main() {
    var code string
    var d time.Duration
    var siwagoObj *siwago.SiwaConfig
    var token *siwago.Token
    var err error

    code = "ce47cc89t73da4d3897905f349d404hq5.0.nrrxy.H2Pt0rU0wi0VdumPWM9pEg"
    d = 30 * 24 * time.Hour
    siwagoObj = siwago.GetObject(KID, TEAMID, BUNDLEID, d, "test_nonce")
    err = siwagoObj.SetSecretP8File("/etc/keys/AuthKey_3UHT5POLK9.p8")
    if err != nil {
        fmt.Println(err.Error())
        return
    }

    token, err = siwagoObj.ExchangeAuthCode(code, "")
    //if there was an error and token is not nil, it can be used to get more information about the failure
    //to check if the apple request failed or token validation failed
    if err != nil {
        if token == nil {
            fmt.Println("Error with exchanging token, token is nil.", err.Error())
        } else if token.Error != "" {
            fmt.Println("Error while requesting token error:", token.Error)
        } else if !token.Valid {
            fmt.Println("Invalid token", err.Error(), token.String())
        }
        return
    }

    fmt.Println("Generated Valid Token:", token.String())
}

Documentation

Index

Constants

View Source
const APPLE_AUTH_URL = "https://appleid.apple.com/auth/token"
View Source
const APPLE_KEYS_URL = "https://appleid.apple.com/auth/keys"
View Source
const AUD = "https://appleid.apple.com"

aud The audience registered claim key, the value of which identifies the recipient the JWT is intended for. Since this token is meant for Apple, use https://appleid.apple.com. https://developer.apple.com/documentation/signinwithapplerestapi/generate_and_validate_tokens

View Source
const AUTHORIZATION_CODE = "code"
View Source
const REFRESH_TOKEN = "refresh_token"

Variables

This section is empty.

Functions

This section is empty.

Types

type AppleKey

type AppleKey struct {
	Kty string `json:"kty"`
	Kid string `json:"kid"`
	Use string `json:"use":`
	Alg string `json:"alg"`
	N   string `json:"n"`
	E   string `json:"e"`
}

key object fetched from APPLE_KEYS_URL

type JWTBody

type JWTBody struct {
	Iss string `json:"iss"`
	Iat int64  `json:"iat"`
	Exp int64  `json:"exp"`
	Aud string `json:"aud"`
	Sub string `json:"sub"`
}

struct for JWT Body

type JWTHeader

type JWTHeader struct {
	Alg string `json:"alg"`
	Kid string `json:"kid"`
}

struct for JWT Header

type JWTTokenBody

type JWTTokenBody struct {
	Iss            string `json:"iss"`
	Iat            int64  `json:"iat"`
	Exp            int64  `json:"exp"`
	Aud            string `json:"aud"`
	Sub            string `json:"sub"`
	AtHash         string `json:"at_hash"`
	Email          string `json:"email"`
	EmailVerified  string `json:"email_verified"`
	IsPrivateEmail string `json:"is_private_email"`
	RealUserStatus int64  `json:"real_user_status"`
	AuthTime       int64  `json:"auth_time"`
	Nonce          string `json:"nonce"`
}

struct for JWT Body

type JWTTokenHeader

type JWTTokenHeader struct {
	Alg string `json:"alg"`
	Kid string `json:"kid"`
}

struct for JWT Header

type SiwaConfig

type SiwaConfig struct {
	KeyId           string        //key Id from Certificates, Identifiers & Profiles on developers.apple.com
	TokenDelta      time.Duration //duration for which you would want the generated client_secret jwt token to be valid. Can not be more than 15777000 (6 months in seconds) from the Current Unix Time on the server.
	TeamId          string        //Team Id that is configured with Key, can also ne found in Certificates, Identifiers & Profiles on developers.apple.com
	BundleId        string        //bundleId for product com.companyname.product
	PemFileContents []byte        //contents of the p8 file
	Nonce           string        //nonce is set while making the request to generate authorization_code. If you dont use it, keep it an empty string
}

struct holding various values needed to generate tokens. this should only needed to be initialized once and then can be kept in memory

func GetObject

func GetObject(keyId string, teamId string, bundleId string, d time.Duration, nonce string) *SiwaConfig

helper function to get SiwaConfig object

func (*SiwaConfig) ExchangeAuthCode

func (self *SiwaConfig) ExchangeAuthCode(code string, redirectUri string) (*Token, error)

function to exchange authorization code for id token, access token, refresh token, etc.

func (*SiwaConfig) ExchangeRefreshToken

func (self *SiwaConfig) ExchangeRefreshToken(code string, redirectUri string) (*Token, error)

function to exchange refresh token for access token

func (*SiwaConfig) GetClientSecret

func (self *SiwaConfig) GetClientSecret() (string, error)

get the client_secret

func (*SiwaConfig) GetEncodedJwtBody

func (self *SiwaConfig) GetEncodedJwtBody(bundleId string, teamId string, d time.Duration) (string, error)

function to get encoded jwt body

func (*SiwaConfig) GetEncodedJwtHeader

func (self *SiwaConfig) GetEncodedJwtHeader(keyId string) (string, error)

function to get encoded jwt header

func (*SiwaConfig) SetSecretP8Bytes

func (self *SiwaConfig) SetSecretP8Bytes(p8Contents []byte)

helper function to set secret file contents as bytes this needs to be pem encoded PKCS8 private key same format as the p8 file downloaded from apple -----BEGIN PRIVATE KEY----- jkfweshjdjkhjsbjvguybjebvuewkvbbhj+jbdhbjhbvjhbvjhbvbjvbvjvagcve jkfweshjdjkhjsbjvguybje/vuewkvbbhjdjbdhbjhbvjhbvjhbvbjvbvjvagcve jkfweshjdjkhjsbjvguybjebvuewkvbbhj+jbdhbjhbvjhbvjhbvbjvbvjvagcve jkfweshj -----END PRIVATE KEY-----

func (*SiwaConfig) SetSecretP8File

func (self *SiwaConfig) SetSecretP8File(p8Filename string) error

helper function to set secrets value by filename the function expects full path to the p8 file generated in the keys and certificates section of developer account it should look like this: -----BEGIN PRIVATE KEY----- jkfweshjdjkhjsbjvguybjebvuewkvbbhj+jbdhbjhbvjhbvjhbvbjvbvjvagcve jkfweshjdjkhjsbjvguybje/vuewkvbbhjdjbdhbjhbvjhbvjhbvbjvbvjvagcve jkfweshjdjkhjsbjvguybjebvuewkvbbhj+jbdhbjhbvjhbvjhbvbjvbvjvagcve jkfweshj -----END PRIVATE KEY-----

func (*SiwaConfig) SetSecretP8String

func (self *SiwaConfig) SetSecretP8String(p8Contents string)

helper function to set secret file contents as a string this needs to be pem encoded PKCS8 private key same format as the p8 file downloaded from apple -----BEGIN PRIVATE KEY----- jkfweshjdjkhjsbjvguybjebvuewkvbbhj+jbdhbjhbvjhbvjhbvbjvbvjvagcve jkfweshjdjkhjsbjvguybje/vuewkvbbhjdjbdhbjhbvjhbvjhbvbjvbvjvagcve jkfweshjdjkhjsbjvguybjebvuewkvbbhj+jbdhbjhbvjhbvjhbvbjvbvjvagcve jkfweshj -----END PRIVATE KEY-----

func (*SiwaConfig) ValidateObject

func (self *SiwaConfig) ValidateObject() (bool, error)

function to validate the object

type SiwaIdToken

type SiwaIdToken struct {
	Header    *JWTTokenHeader
	Body      *JWTTokenBody
	Signature []byte
	Valid     bool
}

struct to hold the decoded idtoken

func ValidateIdToken

func ValidateIdToken(aud string, idToken string) (*SiwaIdToken, string)

validates idToken without nonce check

func ValidateIdTokenWithNonce

func ValidateIdTokenWithNonce(aud string, idToken string, nonce string) (*SiwaIdToken, string)

validates idtoken more info: https://developer.apple.com/documentation/signinwithapplerestapi/verifying_a_user

type Token

type Token struct {
	//(Reserved for future use) A token used to access allowed data. Currently, no data set has been defined for access.
	AccessToken string `json:"access_token"`
	//The type of access token. It will always be bearer.
	TokenType string `json:"token_type"`
	//The amount of time, in seconds, before the access token expires.
	ExpiresIn int64 `json:"expires_in"`
	//The refresh token used to regenerate new access tokens. Store this token securely on your server.
	RefreshToken string `json:"refresh_token"`
	//A JSON Web Token that contains the user’s identity information.
	IdToken string `json:"id_token"`
	//Set if ErrorResponse is recieved
	//A string that describes the reason for the unsuccessful request. The string consists of a single allowed value.
	//Possible values: invalid_request, invalid_client, invalid_grant, unauthorized_client, unsupported_grant_type, invalid_scope
	Error string `json:"error"`
	//After the token is fetched from apple, id token is validated
	//this field stores the result of the validation check
	Valid bool `json:"_"`
	//The decoded Id token
	//Holds the decoded JWT Header, Body, Signature and result of validity check
	DecodedIdToken *SiwaIdToken `json:"_"`
}

struct for token returned from apple

func (Token) String

func (self Token) String() string

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL