Skip to content

Commit

Permalink
feat(core): core functions to interact with OIDC APIs (#7)
Browse files Browse the repository at this point in the history
  • Loading branch information
xiaoyijun authored Aug 16, 2022
1 parent 0f69fc9 commit 0e407cf
Show file tree
Hide file tree
Showing 9 changed files with 357 additions and 1 deletion.
87 changes: 87 additions & 0 deletions core/fetch_token.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package core

import (
"net/http"
"net/url"
)

type FetchTokenByAuthorizationCodeOptions struct {
tokenEndpoint string
code string
codeVerifier string
clientId string
redirectUri string
resource string
}

func FetchTokenByAuthorizationCode(client *http.Client, options *FetchTokenByAuthorizationCodeOptions) (CodeTokenResponse, error) {
values := url.Values{
"client_id": {options.clientId},
"redirect_uri": {options.redirectUri},
"code_verifier": {options.codeVerifier},
"code": {options.code},
"grant_type": {"authorization_code"},
}

if options.resource != "" {
values.Add("resource", options.resource)
}

response, requestErr := client.PostForm(options.tokenEndpoint, values)

if requestErr != nil {
return CodeTokenResponse{}, requestErr
}

defer response.Body.Close()

var codeTokenResponse CodeTokenResponse
err := parseDataFromResponse(response, &codeTokenResponse)

if err != nil {
return CodeTokenResponse{}, err
}

return codeTokenResponse, nil
}

type FetchTokenByRefreshTokenOptions struct {
tokenEndpoint string
clientId string
refreshToken string
resource string
scope string
}

func FetchTokenByRefreshToken(client *http.Client, options *FetchTokenByRefreshTokenOptions) (RefreshTokenResponse, error) {
values := url.Values{
"client_id": {options.clientId},
"refresh_token": {options.refreshToken},
"grant_type": {"refresh_token"},
}

if options.resource != "" {
values.Add("resource", options.resource)
}

if options.scope != "" {
values.Add("scope", options.scope)
}

response, requestErr := client.PostForm(options.tokenEndpoint, values)

if requestErr != nil {
return RefreshTokenResponse{}, requestErr
}

defer response.Body.Close()

var refreshTokenResponse RefreshTokenResponse
err := parseDataFromResponse(response, &refreshTokenResponse)

if err != nil {
return RefreshTokenResponse{}, err
}

return refreshTokenResponse, nil
}
101 changes: 101 additions & 0 deletions core/fetch_token_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package core

import (
"encoding/json"
"net/http"
"testing"

"github.com/google/go-cmp/cmp"
"github.com/jarcoal/httpmock"
)

func TestFetchTokenByAuthorizationCode(t *testing.T) {
httpmock.Activate()
defer httpmock.DeactivateAndReset()

tokenEndpoint := "http://example.com/oidc/token"
mockResponse := `{` +
`"access_token": "access_token",` +
`"refresh_token": "refresh_token",` +
`"id_token": "id_token",` +
`"scope": "openid offline_access",` +
`"expires_in": 3600` +
`}`

httpmock.RegisterResponder(
"POST",
tokenEndpoint,
httpmock.NewStringResponder(200, mockResponse),
)

client := &http.Client{}
options := &FetchTokenByAuthorizationCodeOptions{
tokenEndpoint: tokenEndpoint,
code: "code",
codeVerifier: "codeVerifier",
clientId: "clientId",
redirectUri: "redirectUri",
resource: "resource",
}

token, fetchError := FetchTokenByAuthorizationCode(client, options)
if fetchError != nil {
t.Fatalf(fetchError.Error())
}

var expectedToken CodeTokenResponse
unmarshalErr := json.Unmarshal([]byte(mockResponse), &expectedToken)

if unmarshalErr != nil {
t.Fatalf(unmarshalErr.Error())
}

if !cmp.Equal(token, expectedToken) {
t.Fatalf("token does not match expected result")
}
}

func TestFetchTokenByRefreshToken(t *testing.T) {
httpmock.Activate()
defer httpmock.DeactivateAndReset()

tokenEndpoint := "http://example.com/oidc/token"
mockResponse := `{` +
`"access_token": "access_token",` +
`"refresh_token": "refresh_token",` +
`"id_token": "id_token",` +
`"scope": "openid offline_access",` +
`"expires_in": 3600` +
`}`

httpmock.RegisterResponder(
"POST",
tokenEndpoint,
httpmock.NewStringResponder(200, mockResponse),
)

client := &http.Client{}
options := &FetchTokenByRefreshTokenOptions{
tokenEndpoint: tokenEndpoint,
clientId: "clientId",
refreshToken: "refresh_token",
resource: "resource",
scope: "openid offline_access",
}

token, fetchError := FetchTokenByRefreshToken(client, options)
if fetchError != nil {
t.Fatalf(fetchError.Error())
}

var expectedToken RefreshTokenResponse
unmarshalErr := json.Unmarshal([]byte(mockResponse), &expectedToken)

if unmarshalErr != nil {
t.Fatalf(unmarshalErr.Error())
}

if !cmp.Equal(token, expectedToken) {
t.Fatalf("token does not match expected result")
}
}
5 changes: 4 additions & 1 deletion core/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ module logto.io/core

go 1.19

require gopkg.in/square/go-jose.v2 v2.6.0
require (
github.com/jarcoal/httpmock v1.2.0
gopkg.in/square/go-jose.v2 v2.6.0
)

require github.com/stretchr/testify v1.8.0 // indirect

Expand Down
3 changes: 3 additions & 0 deletions core/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/jarcoal/httpmock v1.2.0 h1:gSvTxxFR/MEMfsGrvRbdfpRUMBStovlSRLw0Ep1bwwc=
github.com/jarcoal/httpmock v1.2.0/go.mod h1:oCoTsnAz4+UoOUIf5lJOWV2QQIW5UoeUI6aM2YnWAZk=
github.com/maxatome/go-testdeep v1.11.0 h1:Tgh5efyCYyJFGUYiT0qxBSIDeXw0F5zSoatlou685kk=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
Expand Down
24 changes: 24 additions & 0 deletions core/oidc_config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package core

import (
"net/http"
)

func FetchOidcConfig(client *http.Client, endpoint string) (OidcConfigResponse, error) {
response, fetchErr := client.Get(endpoint)

if fetchErr != nil {
return OidcConfigResponse{}, fetchErr
}

defer response.Body.Close()

var config OidcConfigResponse
err := parseDataFromResponse(response, &config)

if err != nil {
return OidcConfigResponse{}, err
}

return config, nil
}
48 changes: 48 additions & 0 deletions core/oidc_config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package core

import (
"encoding/json"
"net/http"
"testing"

"github.com/google/go-cmp/cmp"
"github.com/jarcoal/httpmock"
)

func TestFetchOidcConfig(t *testing.T) {
httpmock.Activate()
defer httpmock.DeactivateAndReset()

endpoint := "http://example.com/oidc/.well-known/openid-configuration"
mockResponse := `{` +
`"authorization_endpoint": "http://example.com/oidc/authorize",` +
`"token_endpoint": "http://example.com/oidc/token",` +
`"end_session_endpoint": "http://example.com/oidc/logout",` +
`"revocation_endpoint": "http://example.com/oidc/revoke",` +
`"jwks_uri": "http://example.com/oidc/jwks",` +
`"issuer": "http://example.com/oidc"` +
`}`

httpmock.RegisterResponder(
"GET",
endpoint,
httpmock.NewStringResponder(200, mockResponse),
)

client := &http.Client{}
config, err := FetchOidcConfig(client, endpoint)
if err != nil {
t.Fatalf(err.Error())
}
var expectedConfig OidcConfigResponse

unmarshalErr := json.Unmarshal([]byte(mockResponse), &expectedConfig)

if unmarshalErr != nil {
t.Fatalf(unmarshalErr.Error())
}

if !cmp.Equal(config, expectedConfig) {
t.Fatalf("config does not match expected result")
}
}
22 changes: 22 additions & 0 deletions core/parse_response.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package core

import (
"encoding/json"
"fmt"
"io"
"net/http"
)

func parseDataFromResponse(response *http.Response, dest interface{}) error {
defer response.Body.Close()
body, err := io.ReadAll(response.Body)
if err != nil {
return err
}

if response.StatusCode != http.StatusOK {
return fmt.Errorf("unexpected status code: %d, response body: %s", response.StatusCode, body)
}

return json.Unmarshal(body, &dest)
}
33 changes: 33 additions & 0 deletions core/revoke.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package core

import (
"fmt"
"net/http"
"net/url"
)

type RevocationOptions struct {
revocationEndpoint string
clientId string
token string
}

func Revoke(client *http.Client, options *RevocationOptions) error {
values := url.Values{
"client_id": {options.clientId},
"token": {options.token},
}
response, fetchErr := client.PostForm(options.revocationEndpoint, values)

if fetchErr != nil {
return fetchErr
}

defer response.Body.Close()

if response.StatusCode != http.StatusOK {
return fmt.Errorf("unexpected status code: %d", response.StatusCode)
}

return nil
}
35 changes: 35 additions & 0 deletions core/revoke_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package core

import (
"net/http"
"testing"

"github.com/jarcoal/httpmock"
)

func TestRevoke(t *testing.T) {
httpmock.Activate()
defer httpmock.DeactivateAndReset()

revocationEndpoint := "http://example.com/oidc/revoke"
mockResponse := `{}`

httpmock.RegisterResponder(
"POST",
revocationEndpoint,
httpmock.NewStringResponder(200, mockResponse),
)

client := &http.Client{}
options := &RevocationOptions{
revocationEndpoint: revocationEndpoint,
clientId: "clientId",
token: "token",
}

err := Revoke(client, options)

if err != nil {
t.Fatalf(err.Error())
}
}

0 comments on commit 0e407cf

Please sign in to comment.