-
Notifications
You must be signed in to change notification settings - Fork 88
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Feature Request] code sample for hosted http service #468
Comments
Hi @gwynforthewyn - I acknowledge that we don't have samples around web api. Will try to help, but today most of the MSAL GO users either focus on CLI applications (where AcquireTokenInteractive comes in) or web api requiring service to service auth, so for service principals, not for users (AcquireTokenByCredential). Both of these are "Confidential Client Apps", meaning that you must establish confidentiality between the service and the identity provider, by sharing a secret or a certificate. This is opposite to Public Client apps, such as CLI, mobile apps, desktop apps which cannot keep secrets. From an OAUTH perspective, we see things as follows: Web Sitee.g. ASP.NET, Spring, Java Servlet, Python Django, Flask, NodeJS Express, NextJS In a web site framework, the backend has the ability to "challenge the user". If a "route" or "controller" requires the user to be logged in, it simply checks if an ID token has been obtained. If it hasn't, it redirects the user to the authorization page. The URI for this page can be obtained via Once tokens are obtained, you need to store them somewhere, e.g. in the session. When the user navigates the website, you always have the ID Token to prove they are logged in. To access downstream APIs, you'd also call Web APIIn this scenario, the user logs in to a client - this can be a CLI or desktop app, a SPA or a web site. The client access then calls your web api, which must ensure the user is authenticated. The web api then calls some downstream API, for example Microsoft Graph. Client ---> Middle Tier API (your webapi) ----> Downstream API (Graph) In this case, the client needs a token for the web api itself - you can register your own API in the Azure Entra Portal. The web api then calls We have an integration test that showcases this in some detail: Please see what flow you need and let me know what other language / framework you are familiar with, and I will find a sample which goes in more detail. |
I would like to second this request. |
I also came here looking for an example but had to implement it myself, looking at the Python examples. See the resulting working code below. It uses Echo with SQLite session store and should be straightforward to port to any other framework. I'm planning to create a proper Echo middleware from this. Not sure if it makes sense to release it open-source as well. If you have any suggestions/fixes for this code - please share. package main
import (
"context"
"fmt"
"net/http"
"encoding/gob"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/confidential"
"github.com/labstack/echo-contrib/session"
"github.com/labstack/echo/v4"
"github.com/michaeljs1990/sqlitestore"
)
// var account confidential.Account
var store *sqlitestore.SqliteStore
func init_sqlitestore() {
var err error
store, err = sqlitestore.NewSqliteStore("sessions.db", "sessions", "/", 3600, []byte("<SecretKey>"))
if err != nil {
panic(err)
}
}
type contextKey string
const (
confidentialClientKey contextKey = "confidentialClient"
// Azure AD Config
redirectURI = "http://localhost:8000/"
clientID = "xxx"
tenantID = "xxx"
clientSecret = "xxx"
authority = "https://login.microsoftonline.com/" + tenantID
// HTTP Config
// Note that Azure AD allows HTTP only for localhost, otherwise HTTPs is requried
http_addr = ":8000"
certFile = ""
keyFile = ""
)
// This can't be a constant because it's a slice of strings
var scopes = []string{"User.Read"}
func ConfidentialClientMiddleware(client *confidential.Client) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
ctx := context.WithValue(c.Request().Context(), confidentialClientKey, client)
c.SetRequest(c.Request().WithContext(ctx))
return next(c)
}
}
}
func mainPage(c echo.Context) error {
session, err := store.Get(c.Request(), "auth-session")
ctx := c.Request().Context()
confidentialClient := ctx.Value(confidentialClientKey).(*confidential.Client)
account, _ := session.Values["account"].(*confidential.Account)
options := []confidential.AcquireSilentOption{}
if account != nil {
options = append(options, confidential.WithSilentAccount(*account))
}
result, err := confidentialClient.AcquireTokenSilent(ctx, scopes, options...)
if err != nil {
if c.QueryParam("code") != "" {
queryParams := c.QueryParams()
for key, values := range queryParams {
fmt.Printf("Query Parameter: %s\n", key)
for _, value := range values {
fmt.Printf("Value: %s\n", value)
}
}
authResult, err := confidentialClient.AcquireTokenByAuthCode(ctx, c.QueryParam("code"), redirectURI, scopes)
if err != nil {
return c.String(http.StatusInternalServerError, "Error acquiring token by auth code: "+err.Error())
}
fmt.Println("AcquireTokenByAuthCode returns: ", authResult)
session.Values["account"] = authResult.Account
err = session.Save(c.Request(), c.Response())
if err != nil {
return c.String(http.StatusInternalServerError, "Error saving session: "+err.Error())
}
return c.Redirect(http.StatusTemporaryRedirect, redirectURI)
}
// cache miss, authenticate with another AcquireToken... method
authURL, err := confidentialClient.AuthCodeURL(ctx, clientID, redirectURI, scopes)
fmt.Println("AuthCodeURL returns: ", authURL)
if err != nil {
return c.String(http.StatusInternalServerError, "Error acquiring auth URL: "+err.Error())
}
return c.Redirect(http.StatusTemporaryRedirect, authURL)
}
// accessToken := result.AccessToken
accessTokenStr := fmt.Sprintf("%#v", result)
return c.String(http.StatusOK, accessTokenStr)
}
func main() {
// confidential clients have a credential, such as a secret or a certificate
cred, err := confidential.NewCredFromSecret(clientSecret)
if err != nil {
// TODO: handle error
}
confidentialClient, err := confidential.New(authority, clientID, cred)
e := echo.New()
init_sqlitestore()
gob.Register(&confidential.Account{})
e.Use(session.Middleware(store))
e.Use(ConfidentialClientMiddleware(&confidentialClient)) // Add the middleware here
e.GET("/", mainPage)
if certFile != "" && keyFile != "" {
e.Logger.Fatal(e.StartTLS(http_addr, certFile, keyFile))
} else {
e.Logger.Fatal(e.Start(http_addr))
}
} |
Were you able to create echo middleware? I would be interested in seeing how you did that. Thanks! |
To future people who are trying to auth with Azure in a hosted http service, I made a go library: |
Is your feature request related to a problem? Please describe.
The Microsoft website for MSAL (https://learn.microsoft.com/en-us/entra/identity-platform/msal-overview#application-types-and-scenarios) says that it's suitable for web apps and web apis. I've been trying to understand how to use msal-go for an oauth proxy that can sit in a hosted environment, authenticate a user and then use on-behalf-of to authenticate the same user to a second API.
The msal-go implementation today might be able to support that, but it's really hard to figure out. The confidential client that implements on-behalf-of doesn't implement an equivalent of AcquireTokenInteractive.
The public client can perform redirects, but they're restricted to localhost, and by default they open the default system browser instead of sending a redirect to the currently used browser; it also doesn't seem to have an equivalent of the on-behalf-of workflow implemented. As best I can tell, the public client's http support is intended for desktop apps, not hosted apps.
Describe the solution you'd like
I'd like code examples or some docs indicating how to use msal-go inside an http service that's intended to be hosted.
Describe alternatives you've considered
I'm currently writing my proxy using raw http calls. I did try several open source oauth proxies, but each was deficient for various reasons. The most promising, oauth2-proxy, has a bunch of issues dating back over 12 months reported against it saying that azure support is broken, for example.
Additional context
I'm an honest user trying my best here, but I've found this library pretty tough to work with in a web context. I'm happy to provide more context, or to be told that MSAL isn't designed for my use-case.
The text was updated successfully, but these errors were encountered: