@@ -28,6 +28,7 @@ import (
2828 "github.com/cozy/cozy-stack/pkg/logger"
2929 "github.com/cozy/cozy-stack/pkg/prefixer"
3030 "github.com/cozy/cozy-stack/web/auth"
31+ "github.com/cozy/cozy-stack/web/bitwarden"
3132 "github.com/cozy/cozy-stack/web/middlewares"
3233 "github.com/cozy/cozy-stack/web/statik"
3334 jwt "github.com/golang-jwt/jwt/v5"
@@ -50,7 +51,7 @@ func Start(c echo.Context) error {
5051 inst .Logger ().WithNamespace ("oidc" ).Infof ("Start error: %s" , err )
5152 return renderError (c , nil , http .StatusNotFound , "Sorry, the context was not found." )
5253 }
53- u , err := makeStartURL (inst .Domain , c .QueryParam ("redirect" ), c .QueryParam ("confirm_state" ), conf )
54+ u , err := makeStartURL (inst .Domain , c .QueryParam ("redirect" ), c .QueryParam ("confirm_state" ), "" , conf )
5455 if err != nil {
5556 return renderError (c , nil , http .StatusNotFound , "Sorry, the server is not configured for OpenID Connect." )
5657 }
@@ -65,13 +66,78 @@ func StartFranceConnect(c echo.Context) error {
6566 inst .Logger ().WithNamespace ("oidc" ).Infof ("StartFranceConnect error: %s" , err )
6667 return renderError (c , nil , http .StatusNotFound , "Sorry, the context was not found." )
6768 }
68- u , err := makeStartURL (inst .Domain , c .QueryParam ("redirect" ), c .QueryParam ("confirm_state" ), conf )
69+ u , err := makeStartURL (inst .Domain , c .QueryParam ("redirect" ), c .QueryParam ("confirm_state" ), "" , conf )
6970 if err != nil {
7071 return renderError (c , nil , http .StatusNotFound , "Sorry, the server is not configured for OpenID Connect." )
7172 }
7273 return c .Redirect (http .StatusSeeOther , u )
7374}
7475
76+ func checkRedirectURIForBitwarden (redirectURI string ) error {
77+ if redirectURI == "" {
78+ return errors .New ("redirect URI is required" )
79+ }
80+ u , err := url .Parse (redirectURI )
81+ if err != nil {
82+ return errors .New ("invalid redirect URI" )
83+ }
84+ if u .Scheme == "cozypass" || u .Scheme == "moz-extension" || u .Scheme == "chrome-extension" {
85+ return nil
86+ }
87+ if u .Scheme == "https" && u .Host == "links.mycozy.cloud" {
88+ return nil
89+ }
90+ return errors .New ("invalid redirect URI" )
91+ }
92+
93+ // BitwardenStart starts the OIDC flow for Bitwarden clients
94+ func BitwardenStart (c echo.Context ) error {
95+ contextName := c .Param ("context" )
96+ redirectURI := c .QueryParam ("redirect_uri" )
97+
98+ if err := checkRedirectURIForBitwarden (redirectURI ); err != nil {
99+ return renderError (c , nil , http .StatusNotFound , "Sorry, the redirect URI is not valid." )
100+ }
101+
102+ conf , err := getGenericConfig (contextName )
103+ if err != nil {
104+ return renderError (c , nil , http .StatusNotFound , "Sorry, the context was not found." )
105+ }
106+ u , err := makeStartURL ("" , redirectURI , "" , contextName , conf )
107+ if err != nil {
108+ return renderError (c , nil , http .StatusNotFound , "Sorry, the server is not configured for OpenID Connect." )
109+ }
110+ return c .Redirect (http .StatusSeeOther , u )
111+ }
112+
113+ // BitwardenExchange handles the POST /oidc/bitwarden/:context route for exchanging
114+ // a delegated code for bitwarden credentials
115+ func BitwardenExchange (c echo.Context ) error {
116+ inst := middlewares .GetInstance (c )
117+
118+ code := c .FormValue ("code" )
119+ if code == "" {
120+ return c .JSON (http .StatusBadRequest , echo.Map {
121+ "error" : "code parameter is required" ,
122+ })
123+ }
124+
125+ sub := getStorage ().GetSub (code )
126+ invalidCode := sub == ""
127+ if sub != inst .OIDCID && sub != inst .Domain {
128+ invalidCode = true
129+ }
130+ if invalidCode {
131+ inst .Logger ().WithNamespace ("oidc" ).Infof ("BitwardenExchange invalid code: %s (%s - %s)" ,
132+ sub , inst .OIDCID , inst .Domain )
133+ return c .JSON (http .StatusBadRequest , echo.Map {
134+ "error" : "invalid code" ,
135+ })
136+ }
137+
138+ return bitwarden .RegisterClientAndReturnTokens (c , inst )
139+ }
140+
75141// Redirect is the route after the Identity Provider has redirected the user to
76142// the stack. The redirection is made to a generic domain, like
77143// oauthcallback.cozy.localhost and the association with an instance is made via a
@@ -85,6 +151,42 @@ func Redirect(c echo.Context) error {
85151 return renderError (c , nil , http .StatusNotFound , "Sorry, the session has expired." )
86152 }
87153
154+ if state .BitwardenContext != "" {
155+ conf , err := getGenericConfig (state .BitwardenContext )
156+ if err != nil {
157+ return renderError (c , nil , http .StatusBadRequest , "No OpenID Connect is configured." )
158+ }
159+ accessToken , err := getToken (conf , c .QueryParam ("code" ))
160+ if err != nil {
161+ logger .WithNamespace ("oidc" ).Errorf ("Error on getToken: %s" , err )
162+ return renderError (c , nil , http .StatusBadGateway , "Error from the identity provider." )
163+ }
164+ params , err := getUserInfo (conf , accessToken )
165+ if err != nil {
166+ return err
167+ }
168+ sub , ok := params ["sub" ].(string )
169+ if ! ok {
170+ logger .WithNamespace ("oidc" ).Errorf ("Missing sub" )
171+ return ErrAuthenticationFailed
172+ }
173+ domain , err := extractDomain (conf , params )
174+ if err != nil {
175+ return renderError (c , nil , http .StatusNotFound , "Sorry, the cozy was not found." )
176+ }
177+
178+ code := getStorage ().CreateCode (sub )
179+ u , err := url .Parse (state .Redirect )
180+ if err != nil {
181+ return renderError (c , nil , http .StatusNotFound , "Sorry, an error occurred." )
182+ }
183+ q := u .Query ()
184+ q .Add ("code" , code )
185+ q .Add ("instance" , domain )
186+ u .RawQuery = q .Encode ()
187+ return c .Redirect (http .StatusSeeOther , u .String ())
188+ }
189+
88190 domain := state .Instance
89191 if contextName , ok := FindLoginDomain (domain ); ok {
90192 conf , err := getGenericConfig (contextName )
@@ -625,12 +727,12 @@ func getFranceConnectConfig(context string) (*Config, error) {
625727 return config , nil
626728}
627729
628- func makeStartURL (domain , redirect , confirm string , conf * Config ) (string , error ) {
730+ func makeStartURL (domain , redirect , confirm , bitwardenContext string , conf * Config ) (string , error ) {
629731 u , err := url .Parse (conf .AuthorizeURL )
630732 if err != nil {
631733 return "" , err
632734 }
633- state := newStateHolder (domain , redirect , confirm , conf .Provider )
735+ state := newStateHolder (domain , redirect , confirm , bitwardenContext , conf .Provider )
634736 if err = getStorage ().Add (state ); err != nil {
635737 return "" , err
636738 }
@@ -948,6 +1050,8 @@ func renderError(c echo.Context, inst *instance.Instance, code int, msg string)
9481050func Routes (router * echo.Group ) {
9491051 router .GET ("/start" , Start , middlewares .NeedInstance , middlewares .CheckOnboardingNotFinished )
9501052 router .GET ("/franceconnect" , StartFranceConnect , middlewares .NeedInstance , middlewares .CheckOnboardingNotFinished )
1053+ router .GET ("/bitwarden/:context" , BitwardenStart )
1054+ router .POST ("/bitwarden/:context" , BitwardenExchange , middlewares .NeedInstance )
9511055 router .GET ("/redirect" , Redirect )
9521056 router .GET ("/login" , Login , middlewares .NeedInstance )
9531057 router .POST ("/twofactor" , TwoFactor , middlewares .NeedInstance )
@@ -1043,7 +1147,7 @@ func LoginDomainHandler(c echo.Context, contextName string) error {
10431147 if err != nil {
10441148 return renderError (c , nil , http .StatusNotFound , "Sorry, the context was not found." )
10451149 }
1046- u , err := makeStartURL (r .Host , "" , "" , conf )
1150+ u , err := makeStartURL (r .Host , "" , "" , "" , conf )
10471151 if err != nil {
10481152 return renderError (c , nil , http .StatusNotFound , "Sorry, the server is not configured for OpenID Connect." )
10491153 }
0 commit comments