1 /* ========================================================================
2  * Copyright 2018 - 2020 Eduardo Chappa
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *     http://www.apache.org/licenses/LICENSE-2.0
9  *
10  *
11  * ========================================================================
12  */
13 
14 long auth_oauth2_client (authchallenge_t challenger,authrespond_t responder, char *base,
15 			char *service,NETMBX *mb,void *stream, unsigned long port,
16 			unsigned long *trial,char *user);
17 
18 AUTHENTICATOR auth_oa2 = {
19   AU_HIDE | AU_SINGLE,		/* hidden */
20   OA2NAME,			/* authenticator name */
21   NIL,				/* always valid */
22   auth_oauth2_client,		/* client method */
23   NIL,				/* server method */
24   NIL				/* next authenticator */
25 };
26 
27 #define OAUTH2_USER	"user="
28 #define OAUTH2_USER_LEN  (5)	/* strlen(OAUTH2_USER) */
29 #define OAUTH2_BEARER	"auth=Bearer "
30 #define OAUTH2_BEARER_LEN  (12)	/* strlen(OAUTH2_BEARER) */
31 
32 /* Client authenticator
33  * Accepts: challenger function
34  *	    responder function
35  *	    SASL service name
36  *	    parsed network mailbox structure
37  *	    stream argument for functions
38  *	    pointer to current trial count
39  *	    returned user name
40  * Returns: T if success, NIL otherwise, number of trials incremented if retry
41  */
42 
auth_oauth2_client(authchallenge_t challenger,authrespond_t responder,char * base,char * service,NETMBX * mb,void * stream,unsigned long port,unsigned long * trial,char * user)43 long auth_oauth2_client (authchallenge_t challenger,authrespond_t responder, char *base,
44 			char *service,NETMBX *mb,void *stream, unsigned long port,
45 			unsigned long *trial,char *user)
46 {
47   char *u;
48   void *challenge;
49   unsigned long clen;
50   long ret = NIL;
51   OAUTH2_S oauth2;
52   int tryanother = 0;	/* try another authentication method */
53 
54   memset((void *) &oauth2, 0, sizeof(OAUTH2_S));
55 				/* snarl if not SSL/TLS session */
56   if (!mb->sslflag && !mb->tlsflag)
57     mm_log ("SECURITY PROBLEM: insecure server advertised AUTH=XOAUTH2",WARN);
58 
59 				/* get initial (empty) challenge */
60   if (base || (challenge = (*challenger) (stream,&clen)) != NULL) {
61     if(base == NIL){
62 	fs_give ((void **) &challenge);
63 	if (clen) {		/* abort if challenge non-empty */
64 	   mm_log ("Server bug: non-empty initial XOAUTH2 challenge",WARN);
65 	   (*responder) (stream,NIL,NIL,0);
66 	   ret = LONGT;		/* will get a BAD response back */
67 	}
68     }
69 
70     /*
71      * the call to mm_login_method is supposed to return the username
72      * and access token. If this is not known by the application, then
73      * we call our internal functions to get a refresh token, access token
74      * and expiration time.
75      *
76      * Programmers note: We always call mm_login_method at least once.
77      * The first call is done with empty parameters and it indicates
78      * we are asking the application to load it the best it can. Then
79      * the application returns the loaded value. If we get it fully loaded
80      * we use the value, but if we don't get it fully loaded, we call
81      * our internal functions to try to fully load it.
82      *
83      * If in the internal call we get it loaded, then we use these values
84      * to log in. At this time we call the app to send back the loaded values
85      * so it can save them for the next time we call. This is done in a
86      * second call to mm_login_method. If we do not get oauth2 back with
87      * fully loaded values we cancel authentication completely. If the
88      * user cannot load this variable, then the user, through the client,
89      * should disable XOAUTH2 as an authentication method and try a new one.
90      *
91      * If we make our internal mm_login_oauth2_c_client_method call,
92      * we might still need to call the client to get the access token,
93      * this is done through a callback declared by the client. If we need
94      * that information, but the callback is not declared, this process
95      * will fail, so we will check if that call is declared as soon as we
96      * know we should start it, and we will only start it if this callback
97      * is declared.
98      *
99      * We start this process by calling the client and loading oauth2
100      * with the required information as best as we can.
101      */
102 
103     mm_login_method (mb, user, (void *) &oauth2, *trial, OA2NAME);
104 
105     if(oauth2.param[OA2_State].value)
106       fs_give((void **) &oauth2.param[OA2_State].value);
107 
108     oauth2.param[OA2_State].value = oauth2_generate_state();
109 
110     /*
111      * If we did not get an access token, try to get one through
112      * our internal functions
113      */
114     if(oauth2.name && oauth2.access_token == NIL){
115        char *RefreshToken = NIL;
116 
117        if(oauth2.param[OA2_RefreshToken].value)
118 	 RefreshToken = cpystr(oauth2.param[OA2_RefreshToken].value);
119 
120        mm_login_oauth2_c_client_method (mb, user, OA2NAME, &oauth2, *trial, &tryanother);
121 
122        /*
123         * if we got an access token from the c_client_method call,
124         * or somehow there was a change in the refresh token, return
125         * it to the client so that it will save it.
126         */
127 
128        if(!tryanother
129 	  && (oauth2.access_token
130 	  || (!RefreshToken && oauth2.param[OA2_RefreshToken].value)
131 	  || (RefreshToken && oauth2.param[OA2_RefreshToken].value
132 	      && strcmp(RefreshToken, oauth2.param[OA2_RefreshToken].value))
133 	  || oauth2.cancel_refresh_token))
134          mm_login_method (mb, user, (void *) &oauth2, *trial, OA2NAME);
135 
136        if(RefreshToken)
137 	  fs_give((void **) &RefreshToken);
138     }
139 
140     /* empty challenge or user requested abort or client does not have info */
141     if(tryanother || !oauth2.access_token) {
142       if (!base) (*responder) (stream,base,NIL,0);
143       *trial = 0;		/* cancel subsequent attempts */
144       ret = base ? NIL : LONGT;		/* will get a BAD response back */
145     }
146     else {
147       unsigned long rlen = OAUTH2_USER_LEN + OAUTH2_BEARER_LEN + 2
148 			   + strlen(user) + strlen(oauth2.access_token) + 1;
149       char *response = (char *) fs_get (rlen + 1);
150       sprintf(response, "%s%s\001%s%s\001\001", OAUTH2_USER, user, OAUTH2_BEARER, oauth2.access_token);
151       if ((*responder) (stream,base,response,rlen)) {
152 	if ((challenge = (*challenger) (stream,&clen)) != NULL)
153 	  fs_give ((void **) &challenge);
154 	else {
155 	  ++*trial;				/* can try again if necessary */
156 	  ret = *trial < 3 ? LONGT : NIL;	/* check the authentication */
157 	  /* When the Access Token expires we fail once, but after we get
158 	   * a new one, we should succeed at the second attempt. If the
159 	   * Refresh Token has expired somehow, we invalidate it if we
160 	   * reach *trial to 3. This forces the process to restart later on.
161 	   */
162 	  if(*trial == 3)
163 	     oauth2.expiration = 0L;
164 	}
165       }
166       fs_give ((void **) &response);
167       if (!ret)
168 	*trial = 65535; 	/* don't retry if bad protocol */
169     }
170   }
171   if(oauth2.param[OA2_Id].value) fs_give((void **) &oauth2.param[OA2_Id].value);
172   if(oauth2.param[OA2_Secret].value) fs_give((void **) &oauth2.param[OA2_Secret].value);
173   if(oauth2.param[OA2_Tenant].value) fs_give((void **) &oauth2.param[OA2_Tenant].value);
174   if(oauth2.param[OA2_State].value) fs_give((void **) &oauth2.param[OA2_State].value);
175   if(oauth2.param[OA2_RefreshToken].value) fs_give((void **) &oauth2.param[OA2_RefreshToken].value);
176   if(oauth2.access_token) fs_give((void **) &oauth2.access_token);
177   return ret;
178 }
179