Production Ready Macros for SAS Application Developers
https://github.com/sasjs/core
mv_registerclient.sas
Go to the documentation of this file.
1 /**
2  @file mv_registerclient.sas
3  @brief Register Client and Secret (admin task)
4  @details When building apps on SAS Viya, an client id and secret is required.
5  This macro will obtain the Consul Token and use that to call the Web Service.
6 
7  more info: https://developer.sas.com/reference/auth/#register
8  and: http://proc-x.com/2019/01/authentication-to-sas-viya-a-couple-of-approaches/
9 
10  The default viyaroot location is /opt/sas/viya/config
11 
12  Usage:
13 
14  %* compile macros;
15  filename mc url "https://raw.githubusercontent.com/sasjs/core/main/all.sas";
16  %inc mc;
17 
18  %* specific client with just openid scope;
19  %mv_registerclient(client_id=YourClient
20  ,client_secret=YourSecret
21  ,scopes=openid
22  )
23 
24  %* generate random client details with all scopes;
25  %mv_registerclient(scopes=openid *)
26 
27  %* generate random client with 90/180 second access/refresh token expiry;
28  %mv_registerclient(scopes=openid *
29  ,access_token_validity=90
30  ,refresh_token_validity=180
31  )
32 
33  @param client_id= The client name. Auto generated if blank.
34  @param client_secret= Client secret Auto generated if client is blank.
35  @param scopes= list of space-seperated unquoted scopes (default is openid)
36  @param grant_type= valid values are "password" or "authorization_code" (unquoted)
37  @param outds= the dataset to contain the registered client id and secret
38  @param access_token_validity= The duration of validity of the access token
39  in seconds. A value of DEFAULT will omit the entry (and use system default)
40  @param refresh_token_validity= The duration of validity of the refresh token
41  in seconds. A value of DEFAULT will omit the entry (and use system default)
42  @param name= A human readable name for the client
43  @param required_user_groups= A list of group names. If a user does not belong
44  to all the required groups, the user will not be authenticated and no tokens
45  are issued to this client for that user. If this field is not specified,
46  authentication and token issuance proceeds normally.
47  @param autoapprove= During the auth step the user can choose which scope to
48  apply. Setting this to true will autoapprove all the client scopes.
49  @param use_session= If true, access tokens issued to this client will be
50  associated with an HTTP session and revoked upon logout or time-out.
51  @param outjson= A dataset containing the lines of JSON submitted. Useful
52  for debugging. Default= _null_.
53 
54  @version VIYA V.03.04
55  @author Allan Bowe
56  @source https://github.com/sasjs/core
57 
58  <h4> Dependencies </h4>
59  @li mp_abort.sas
60  @li mf_getplatform.sas
61  @li mf_getuniquefileref.sas
62  @li mf_getuniquelibref.sas
63  @li mf_loc.sas
64  @li mf_getquotedstr.sas
65  @li mf_getuser.sas
66 
67 **/
68 
69 %macro mv_registerclient(client_id=
70  ,client_secret=
71  ,client_name=DEFAULT
72  ,scopes=openid
73  ,grant_type=authorization_code|refresh_token
74  ,required_user_groups=
75  ,autoapprove=
76  ,use_session=
77  ,outds=mv_registerclient
78  ,access_token_validity=DEFAULT
79  ,refresh_token_validity=DEFAULT
80  ,outjson=_null_
81  );
82 %local consul_token fname1 fname2 fname3 libref access_token url;
83 
84 %if client_name=DEFAULT %then %let client_name=
85  Generated by %mf_getuser() on %sysfunc(datetime(),datetime19.) using SASjs;
86 
87 options noquotelenmax;
88 /* first, get consul token needed to get client id / secret */
89 data _null_;
90  infile "%mf_loc(VIYACONFIG)/etc/SASSecurityCertificateFramework/tokens/consul/default/client.token";
91  input token:$64.;
92  call symputx('consul_token',token);
93 run;
94 
95 %local base_uri; /* location of rest apis */
96 %let base_uri=%mf_getplatform(VIYARESTAPI);
97 
98 /* request the client details */
99 %let fname1=%mf_getuniquefileref();
100 proc http method='POST' out=&fname1
101  url="&base_uri/SASLogon/oauth/clients/consul?callback=false%str(&)serviceId=app";
102  headers "X-Consul-Token"="&consul_token";
103 run;
104 
105 %let libref=%mf_getuniquelibref();
106 libname &libref JSON fileref=&fname1;
107 
108 /* extract the token */
109 data _null_;
110  set &libref..root;
111  call symputx('access_token',access_token,'l');
112 run;
113 
114 /**
115  * register the new client
116  */
117 %let fname2=%mf_getuniquefileref();
118 %if x&client_id.x=xx %then %do;
119  %let client_id=client_%sysfunc(ranuni(0),hex16.);
120  %let client_secret=secret_%sysfunc(ranuni(0),hex16.);
121 %end;
122 
123 %let scopes=%sysfunc(coalescec(&scopes,openid));
124 %let scopes=%mf_getquotedstr(&scopes,QUOTE=D,indlm=|);
125 %let grant_type=%mf_getquotedstr(&grant_type,QUOTE=D,indlm=|);
126 %let required_user_groups=%mf_getquotedstr(&required_user_groups,QUOTE=D,indlm=|);
127 
128 data _null_;
129  file &fname2;
130  length clientid clientsecret clientname scope grant_types reqd_groups
131  autoapprove $256.;
132  clientid='"client_id":'!!quote(trim(symget('client_id')));
133  clientsecret=',"client_secret":'!!quote(trim(symget('client_secret')));
134  clientname=',"name":'!!quote(trim(symget('client_name')));
135  scope=',"scope":['!!symget('scopes')!!']';
136  grant_types=symget('grant_type');
137  if grant_types = '""' then grant_types ='';
138  grant_types=cats(',"authorized_grant_types": [',grant_types,']');
139  reqd_groups=symget('required_user_groups');
140  if reqd_groups = '""' then reqd_groups ='';
141  else reqd_groups=cats(',"required_user_groups":[',reqd_groups,']');
142  autoapprove=trim(symget('autoapprove'));
143  if not missing(autoapprove) then autoapprove=cats(',"autoapprove":',autoapprove);
144  use_session=trim(symget('use_session'));
145  if not missing(use_session) then use_session=cats(',"use_session":',use_session);
146 
147  put '{' clientid ;
148  put clientsecret ;
149  put clientname;
150  put scope;
151  put grant_types;
152  if not missing(reqd_groups) then put reqd_groups;
153  put autoapprove;
154  put use_session;
155 %if &access_token_validity ne DEFAULT %then %do;
156  put ',"access_token_validity":' "&access_token_validity";
157 %end;
158 %if &refresh_token_validity ne DEFAULT %then %do;
159  put ',"refresh_token_validity":' "&refresh_token_validity";
160 %end;
161 
162  put ',"redirect_uri": "urn:ietf:wg:oauth:2.0:oob"';
163  put '}';
164 run;
165 
166 %let fname3=%mf_getuniquefileref();
167 proc http method='POST' in=&fname2 out=&fname3
168  url="&base_uri/SASLogon/oauth/clients";
169  headers "Content-Type"="application/json"
170  "Authorization"="Bearer &access_token";
171 run;
172 
173 /* show response */
174 %local err;
175 %let err=NONE;
176 data _null_;
177  infile &fname3;
178  input;
179  if _infile_=:'{"err'!!'or":' then do;
180  length message $32767;
181  message=scan(_infile_,-2,'"');
182  call symputx('err',message,'l');
183  end;
184 run;
185 %if "&err" ne "NONE" %then %do;
186  %put %str(ERR)OR: &err;
187 %end;
188 
189 /* prepare url */
190 %if %index(%superq(grant_type),authorization_code) %then %do;
191  data _null_;
192  if symexist('_baseurl') then do;
193  url=symget('_baseurl');
194  if subpad(url,length(url)-9,9)='SASStudio'
195  then url=substr(url,1,length(url)-11);
196  else url="&systcpiphostname";
197  end;
198  else url="&systcpiphostname";
199  call symputx('url',url);
200  run;
201 %end;
202 
203 %put Please provide the following details to the developer:;
204 %put ;
205 %put CLIENT_ID=&client_id;
206 %put CLIENT_SECRET=&client_secret;
207 %put GRANT_TYPE=&grant_type;
208 %put;
209 %if %index(%superq(grant_type),authorization_code) %then %do;
210  /* cannot use base_uri here as it includes the protocol which may be incorrect externally */
211  %put NOTE: The developer must also register below and select 'openid' to get the grant code:;
212  %put NOTE- ;
213  %put NOTE- &url/SASLogon/oauth/authorize?client_id=&client_id%str(&)response_type=code;
214  %put NOTE- ;
215 %end;
216 
217 data &outds;
218  client_id=symget('client_id');
219  client_secret=symget('client_secret');
220  error=symget('err');
221 run;
222 
223 data &outjson;
224  infile &fname2;
225  input;
226  line=_infile_;
227 run;
228 
229 /* clear refs */
230 filename &fname1 clear;
231 filename &fname2 clear;
232 filename &fname3 clear;
233 libname &libref clear;
234 
235 %mend;