diff --git a/GolangBackendOAuth2/.gitignore b/GolangBackendOAuth2/.gitignore new file mode 100644 index 0000000..0bd7c30 --- /dev/null +++ b/GolangBackendOAuth2/.gitignore @@ -0,0 +1,2 @@ +ion-api-credentials.json +src/infor.com/sample/sample diff --git a/GolangBackendOAuth2/README.md b/GolangBackendOAuth2/README.md index b672c1c..feeac88 100644 --- a/GolangBackendOAuth2/README.md +++ b/GolangBackendOAuth2/README.md @@ -1,57 +1,60 @@ Go applications ------------------- -Golang provides package golang.org/x/oauth2 to implement the OAuth2.0 protocol. - -**Make OAuth 2.0 configuration** -First step is to define a configuration. Reference to downloaded credentials properties will be used in all code examples. - - conf := &oauth2.Config{ - ClientID: , - ClientSecret: , - Scopes: []string{ - "openid profile", - }, - Endpoint: oauth2.Endpoint{ - AuthURL: + , - TokenURL: + , - }, - } +This sample application demonstrates how to authenticate with the Infor ION API using OAuth2 in a Go backend application. It uses the `golang.org/x/oauth2` package to handle the OAuth2 flow. -**Obtain tokens** -Now it is ready to obtain tokens. Token struct in Go contains both access and refresh tokens. +**Configuration** - tok, err := conf.PasswordCredentialsToken(oauth2.NoContext, , ) - if err != nil { - // handle error - } +All credentials and endpoint information are stored in the `ion-api-credentials.json` file. Before running the application, you must populate this file with the correct values for your environment. -**Create HTTP client and make a request** -OAuth 2.0 configuration struct has also a method to create HTTP client for you. +```json +{ + "ci": "", + "cs": "", + "pu": "", + "oa": "", + "ot": "", + "or": "", + "saak": "", + "sask": "", + "iu": "", + "ti": "" +} +``` - client := conf.Client(oauth2.NoContext, tok) - - resp, err := client.Get() - if err != nil { - // handle error - } +The application loads these credentials at startup. -_Note:_ you do not need to refresh token manually, client cares about and will do it [automatically.](https://godoc.org/golang.org/x/oauth2#Config.Client) +**Obtain tokens** -**Revoke tokens** -The package does not provide methods to revoke any token. You can do it, calling revoke service directly. +The application uses the `PasswordCredentialsToken` function to exchange the service account credentials for an access token and a refresh token. - resp, err := http.Get( + + "?token=" + tok.AccessToken) + tok, err := conf.PasswordCredentialsToken(oauth2.NoContext, creds.Saak, creds.Sask) if err != nil { // handle error } +**Create HTTP client and make a request** +Once the tokens are obtained, an HTTP client is created to make authenticated API requests. The `oauth2` package automatically handles adding the `Authorization` header with the access token to each request. + client := conf.Client(oauth2.NoContext, tok) + resp, err := client.Get() + if err != nil { + // handle error + } +_Note:_ The client will automatically use the refresh token to obtain a new access token if the original one has expired. +**Revoke tokens** +When the application no longer needs to make API calls, it's important to revoke the refresh token to invalidate the session. This is a security best practice. + if tok.RefreshToken != "" { + err = revokeToken(tok.RefreshToken, "refresh_token", revokeURL, creds.Ci, creds.Cs) + if err != nil { + log.Println("Error revoking refresh token:", err) + } + } - +The `revokeToken` function sends a POST request to the token revocation endpoint to invalidate the refresh token. diff --git a/GolangBackendOAuth2/ion-api-credentials.json.example b/GolangBackendOAuth2/ion-api-credentials.json.example new file mode 100644 index 0000000..5daa6ba --- /dev/null +++ b/GolangBackendOAuth2/ion-api-credentials.json.example @@ -0,0 +1,12 @@ +{ + "ci": "", + "cs": "", + "pu": "", + "oa": "", + "ot": "", + "or": "", + "saak": "", + "sask": "", + "iu": "", + "ti": "" +} diff --git a/GolangBackendOAuth2/src/infor.com/sample/BackendOAuth2.go b/GolangBackendOAuth2/src/infor.com/sample/BackendOAuth2.go index d7aeb94..7565f22 100644 --- a/GolangBackendOAuth2/src/infor.com/sample/BackendOAuth2.go +++ b/GolangBackendOAuth2/src/infor.com/sample/BackendOAuth2.go @@ -1,59 +1,146 @@ package main import ( + "encoding/json" "golang.org/x/oauth2" + "io/ioutil" "log" "net/http" - "io/ioutil" + "os" + "path" + "strings" + "fmt" + "net/url" + "context" + "flag" ) -const mingleEndpoint string = "https://mingleinteg01-ionapi.mingledev.infor.com/AWSION_DEV/Mingle/" -const mingleSocial string = mingleEndpoint + "SocialService.SVC" +// IonApiCredentials matches the structure of the JSON credentials file +type IonApiCredentials struct { + Ci string `json:"ci"` + Cs string `json:"cs"` + Pu string `json:"pu"` + Oa string `json:"oa"` + Ot string `json:"ot"` + Or string `json:"or"` + Saak string `json:"saak"` + Sask string `json:"sask"` + Iu string `json:"iu"` + Ti string `json:"ti"` +} + +// loadCredentials reads and parses the credentials file +func loadCredentials(file string) (IonApiCredentials, error) { + var creds IonApiCredentials + jsonFile, err := os.Open(file) + if err != nil { + return creds, err + } + defer jsonFile.Close() + + byteValue, err := ioutil.ReadAll(jsonFile) + if err != nil { + return creds, err + } + if err := json.Unmarshal(byteValue, &creds); err != nil { + return creds, err + } + return creds, nil +} func main() { + credentialsPath := flag.String("creds", "ion-api-credentials.json.example", "Path to the credentials file") + flag.Parse() + + println("Loading credentials...") + creds, err := loadCredentials(*credentialsPath) + if err != nil { + log.Fatalf("Failed to load credentials from %s: %v", *credentialsPath, err) + } println("Getting OAuth 2.0 token...") conf := &oauth2.Config{ - ClientID: "AWSION_DEV~k0UIGQzDF0g0OCK37MGd_Dt-3WKELV1d1rP-OKv1DWc", - ClientSecret: "A_9Sz4zwZzGkYCFkUKr9IjHT_87gi4fsmipW1-_77f5jLyaXT800GvKFFLVs40jvTm-rY95ZycahP0WeT_pD2Q", + ClientID: creds.Ci, + ClientSecret: creds.Cs, Scopes: []string{""}, Endpoint: oauth2.Endpoint{ - AuthURL: "https://mingleinteg01-sso.mingledev.infor.com/AWSION_DEV/as/authorization.oauth2", - TokenURL: "https://mingleinteg01-sso.mingledev.infor.com/AWSION_DEV/as/token.oauth2", + AuthURL: path.Join(creds.Pu, creds.Oa), + TokenURL: path.Join(creds.Pu, creds.Ot), }, } - tok, err := conf.PasswordCredentialsToken(oauth2.NoContext, - "AWSION_DEV#Bhqfl0x3ec9AWOx5_JBY96KD9fcVbQKREMe83ozDAxc3qrgXLk49eeFSMUSOND6AN2D8b6MqmltN4VbAfW24Nw", - "f7pFGxW-pXRC9R1DpZM0MM33ZiNxDgqRUesCbemorUO0hKlatzJqkrZOrlQgLtjen-S1RYhfXOwfXvY9etfHyg") + tok, err := conf.PasswordCredentialsToken(context.Background(), creds.Saak, creds.Sask) if err != nil { log.Fatal(err) } println("Received token: ", tok.TokenType, tok.AccessToken) - println("Making Ming.le API test query...") - req, err := http.NewRequest("GET", mingleSocial+"/User/Detail/", nil) + println("Making API test query...") + apiUrl := path.Join(creds.Iu, creds.Ti, "/M3/m3api-rest/execute/MMS200MI/GetServerTime") + req, err := http.NewRequest("GET", apiUrl, nil) if err != nil { log.Fatal(err) } req.Header.Add("Accept", "application/json") - clientMingle := conf.Client(oauth2.NoContext, tok) - respMingle, err := clientMingle.Do(req) + client := conf.Client(context.Background(), tok) + resp, err := client.Do(req) if err != nil { log.Fatal(err) } else { - data, err := readAllData(respMingle) + data, err := readAllData(resp) if err != nil { log.Fatal(err) } println("Test response: ", len(string(data))) } + if tok.RefreshToken != "" { + println("Revoking refresh token...") + revokeURL := path.Join(creds.Pu, creds.Or) + err = revokeToken(tok.RefreshToken, "refresh_token", revokeURL, creds.Ci, creds.Cs) + if err != nil { + log.Println("Error revoking refresh token:", err) + } + } + println("Done.") } func readAllData(resp *http.Response) ([]byte, error) { defer resp.Body.Close() return ioutil.ReadAll(resp.Body) +} + +func revokeToken(token string, tokenType string, revokeURL string, clientID string, clientSecret string) error { + client := &http.Client{} + data := url.Values{} + data.Set("token", token) + data.Set("token_type_hint", tokenType) + + req, err := http.NewRequest("POST", revokeURL, strings.NewReader(data.Encode())) + if err != nil { + return err + } + req.Header.Add("Content-Type", "application/x-www-form-urlencoded") + req.SetBasicAuth(clientID, clientSecret) + + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + log.Printf("Failed to read response body while revoking token. Status: %s", resp.Status) + return fmt.Errorf("failed to revoke token: %s", resp.Status) + } + log.Printf("Failed to revoke token. Status: %s, Body: %s", resp.Status, string(body)) + return fmt.Errorf("failed to revoke token: %s", resp.Status) + } + + println("Successfully revoked token:", tokenType) + return nil } \ No newline at end of file diff --git a/GolangBackendOAuth2/src/infor.com/sample/go.mod b/GolangBackendOAuth2/src/infor.com/sample/go.mod new file mode 100644 index 0000000..d616f72 --- /dev/null +++ b/GolangBackendOAuth2/src/infor.com/sample/go.mod @@ -0,0 +1,5 @@ +module infor.com/sample + +go 1.24.3 + +require golang.org/x/oauth2 v0.34.0 diff --git a/GolangBackendOAuth2/src/infor.com/sample/go.sum b/GolangBackendOAuth2/src/infor.com/sample/go.sum new file mode 100644 index 0000000..d93f662 --- /dev/null +++ b/GolangBackendOAuth2/src/infor.com/sample/go.sum @@ -0,0 +1,2 @@ +golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= +golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=