package auth import ( "context" "errors" "fmt" "net/url" "strings" "time" "github.com/evcc-io/evcc/api" "github.com/evcc-io/evcc/server/providerauth" "github.com/evcc-io/evcc/util" "golang.org/x/oauth2" ) func init() { registry.AddCtx("demo", NewDemoFromConfig) } type demo struct { token *oauth2.Token server string method string redirectUri string onlineC chan<- bool } var demoInstance *demo func NewDemoFromConfig(_ context.Context, other map[string]any) (oauth2.TokenSource, error) { var cc struct { Server string Method string RedirectUri string Secret string Scope string // advanced auth param, used by e2e tests } if err := util.DecodeOther(other, &cc); err != nil { return nil, err } return NewDemo(cc.Server, cc.Method, cc.RedirectUri, cc.Secret) } func NewDemo(server, method, redirectUri, secret string) (oauth2.TokenSource, error) { if secret != "topsecret" { return nil, errors.New("invalid secret") } // reuse instance (similar to oauth.go getInstance pattern) if demoInstance != nil { // update existing instance with new values demoInstance.server = server demoInstance.method = method demoInstance.redirectUri = redirectUri return demoInstance, nil } demoInstance = &demo{ server: server, method: method, redirectUri: redirectUri, } onlineC, err := providerauth.Register("demo", demoInstance) if err != nil { return nil, err } demoInstance.onlineC = onlineC // Send initial auth status demoInstance.setOnline(false) return demoInstance, nil } // setOnline notifies the auth handler without blocking; see OAuth.setOnline. func (o *demo) setOnline(online bool) { if o.onlineC == nil { return } select { case o.onlineC <- online: default: } } func (o *demo) Token() (*oauth2.Token, error) { if o.token == nil { return nil, api.LoginRequiredError("demo") } return o.token, nil } func (o *demo) Login(state string) (string, *oauth2.DeviceAuthResponse, error) { // Validate server URL has proper scheme if !strings.HasPrefix(o.server, "http://") && !strings.HasPrefix(o.server, "https://") { return "", nil, fmt.Errorf("server must start with http:// or https://") } // Validate redirect URI has proper scheme if !strings.HasPrefix(o.redirectUri, "http://") && !strings.HasPrefix(o.redirectUri, "https://") { return "", nil, fmt.Errorf("redirectUri must start with http:// or https://") } // Build mock login URL with state and redirectUri (complete callback URL) values := url.Values{} values.Set("state", state) values.Set("redirectUri", o.redirectUri) mockLoginURL := fmt.Sprintf("%s/mock-login?%s", o.server, values.Encode()) if o.method == "device-code" { // Device code flow: URI comes from DeviceAuthResponse return "", &oauth2.DeviceAuthResponse{ UserCode: "12AB345", VerificationURI: mockLoginURL, Expiry: time.Now().Add(10 * time.Minute), }, nil } // Redirect flow: URI in first return value return mockLoginURL, nil, nil } func (o *demo) Logout() error { o.token = nil o.setOnline(false) return nil } func (o *demo) HandleCallback(params url.Values) error { // Extract code from callback parameters code := params.Get("code") if code == "" { return fmt.Errorf("missing code parameter") } // Create token based on code (for demo, we use a fixed token) o.token = &oauth2.Token{ AccessToken: code, // Use the code as the access token Expiry: time.Now().Add(24 * time.Hour), } // Notify that authentication succeeded o.setOnline(true) return nil } func (o *demo) Authenticated() bool { return o.token != nil } func (o *demo) DisplayName() string { return "Demo Auth" }